2020 KCTF秋季賽 | 第五題設計及解題思路
2020 KCTF正在如火如荼的進行中!已有超過5萬人圍觀!第五題《緊急救援》歷時4天,最終以 2 支戰隊破解的成績落下帷幕。
在賽題即將結束的最後一天凌晨,k1ee 戰隊逆轉直上,在凌晨2點42分攻擊成功!併成功升至第二名的寶座,恭喜!ReZero戰隊隨後也在7點左右提交答案,成為第二支挑戰成功的戰隊,守住了第一的位置。
賽場如戰場,各位選手不輕言棄,更是體現了高手過招的魅力!刀光劍影,是比拼更是成就,一招一式盡顯各位英雄本色!
那麼這道讓眾多團隊都倍感遺憾的賽題,作者究竟都使用了哪些技術與tricks呢?讓我們一起來看看吧!
一. 題目簡介
逃離魔爪的你拖著疲憊的身心打算先找個地方安頓下來,瞭解目前的情況。
此時,腦海中第一個浮現的就是“他”——你的駭客摯友肖恩。肖恩是你任職於國家網路安全域性時交到的唯一的朋友,跟你一樣也是國安局的王牌。現在仍在職的他自然是收到了“破曉”的電子邀請函的。
但他不像你,自幼父母在事故中喪生,孤家寡人,無牽無掛。正是因為自己的女兒患先天疾病,被Norns判定為無資格登上方舟者,肖恩才毅然選擇和家人們一起留在地球。
其實在“破曉”上檢測到來自這座城市的異常訊號時,你就猜測可能是肖恩的手筆了。
驅車趕往肖恩家,摁響門鈴,迎接你的是肖恩的妻子。看到你的瞬間她瞪大了眼睛,驚訝、疑惑多種複雜的情緒快速變換著。你越過她朝家裡掃視了一圈,心中的石頭總算落下了,他們一家三口都平安無事,而且看起來都很正常。
但進了家門,你卻越發覺得平靜之下,暗藏波瀾。肖恩妻女臉上的笑容總是很僵硬,平時對肖恩愛意滿滿的眼神如今卻流露著不著痕跡的恐懼……肖恩卻不以為然,微笑著夾起肉放在你的碗裡。
飯桌上那說不出是溫馨還是詭異的氣氛讓可口的飯菜都變得有些難以下嚥。你抬頭對上肖恩的視線,與剛才相比沒有絲毫變化,就連嘴上揚起的弧度也如此標準而刻板。
你起了一身雞皮疙瘩,再望向肖恩的妻子,她沒有出聲但好像在暗示你什麼。你努力辨認她的口型,似乎是?
“快……逃……”
你站起來的瞬間,肖恩就朝你迎頭一擊,試圖將你制服。你抄起刀與他對峙,發現他的傷口流出了仿生血液。
控制住他後,肖恩妻子告訴你,地球沒有毀滅,但不知為何仿生人突然暴動,地球上大部分人類都被他們控制,而真正的肖恩也被他們抓走。
趕快利用仿生人肖恩的晶片尋找線索,定位肖恩的地理位置,解救他!
二. 出題團隊簡介
三. 看雪專家點評
點評由 HHHso 提供
本題防守方採用了多層VM的設計思路,比賽中VM常見,多層VM罕見,讓攻擊者欲罷不能。
當攻擊者們在尋找快速方案如手解等方式撥開第一層VM後,發現後面還是VM,套娃感凸現,一望無際,定力稍有不慎,可能就會被嚇住。這時候需要攻擊者使用各種看家本領透視VM業務邏輯,層層擊破,得到可閱讀的程式碼。
當VM面紗都揭開時,高層VM對底層VM的執行時修改以及暗藏邏輯的觸發聯動機制也需要一一解開,才能聯合起來得到完整的驗證邏輯進行解題。高層VM對支撐其運作的低層VM修改和聯動,除了套娃感,也略帶虛擬逃逸感。總體設計非常優秀,相信各位攻擊者都有所得。
四. 設計思路
設計思路由作者 ccfer 提供
題目型別:Window平臺CrackMe
系統需求:WIN10/64
成功提示:Success!
題目答案:
AECCC0DE0C3256F75E37A6BFA227A2ED3D54AC964B43544632303230466C6167826B49EB0A305A72C2E92C18A0901280F47791BAE00932B0
設計說明
遊戲原型是熄燈拼圖,透過若干次的操作,每次切換某方塊及其上下左右四個相鄰方塊的開關狀態,直到全部狀態完成翻轉。
首先是個10*10的熄燈拼圖,這規模小能直接搜到答案,需要最少44次操作,實現完全翻轉,得到的解(開關操作位置圖)是:
防止直接搜到答案,後面又加了一個40*40的,需要解線性方程組(用sage很快就跑出結果)。
詳情參考:
hxxps://mathworld.wolfram.com/LightsOutPuzzle.html
然後這個10*10的解7位一組轉成15個位元組,每個位元組最高位補充了校驗位,最後再補充1個位元組,變成16位元組的key,用k表示。
為了壓縮輸入長度,40*40的解因為也是左右上下及對角線對稱,可以用一個八分之一的區域性三角形經三次映象得到40*40的矩陣,輸入只需要(1+19)*19/2=190位,8位一組轉成24個位元組用e表示。
整個完整key是56位元組,可以表示為:
aaaakkkkkkkkkkkkkkkkbbbbccccddddeeeeeeeeeeeeeeeeeeeeeeee
其中aaaa、bbbb、cccc、dddd是固定常數,a部分的DWORD經過8輪Koopman多項式的crc運算。
k、b、c、d部分的28位元組組成14個WORD,用ECC做一次點乘運算,ECC引數:p=65011,a=65008,b=6,G=(1,2)。
這個微型ECC直接窮舉即可求解,每個點會有兩個解,收集所有最高位排除多解
難度提升
演算法用C語言實現,然後用一個簡單的C編譯器編譯成opcode,再解釋執行,相當於一個簡單的虛擬機器,感覺這個虛擬機器也是相當簡單了,於是把直譯器也編譯一遍,做一次巢狀的解釋執行,大概意思是:從前有座山,山上有座廟,廟裡有個老和尚,老和尚在給小和尚講故事,故事講的是......
解題思路
虛擬機器比較簡單,先把第一層opcode還原出來,發現邏輯竟然等同於直譯器自身,只是opcode定義有些亂序,然後再還原出第二層和第三層,最後第四層裡就只剩下簡單的驗證邏輯瞭然後ECC的p只有16位,直接窮舉即可求解。
LightsOut求解直接用sage也是迅速得出結果。
crc暴力窮舉會慢點,不過4位元組得相當於可逆,有快速破解方法。
五. 解題思路
設計思路由作者 k1ee 提供
緊急救援
我真的沒做過虛擬機器題,這題套了4層虛擬機器,屬實給力,不能再用以前的手打虛擬機器方法了,必須程式化。首先拖入IDA分析。
輸入一段Hex Text,轉為Hex Bytes。
建立緩衝區,複製虛擬機器指令到如圖位置。隨後按以下結構體構造了虛擬機器的引數
struct vm_sub { int param1; //6, 6, 6, 3 int param2; //ins1, ins2, ins3, input_hex char* vm_ins; int size; int idk_0; int id; int idk7; int idk8; }; struct vm_fin { unsigned char* input_hex; int* len_buf; vm_sub* vmsub; }; struct vm_context { vm_sub subs[4]; vm_fin fin; };
隨後傳入第一個虛擬機器上下文引數(vm_sub
)開始執行虛擬機器,並由結果進行輸出。
進入虛擬機器函式,廢話和彎路我就不多說了,直接分析可知
這是典型的壓棧,再看後續指令
基本都是透過堆疊進行計算。經過兩天的彎路後,我最終決定透過KeyStone還原各層虛擬機器的原始碼。由於1,2,3層虛擬機器的指令僅僅是替換而已,因此這裡只分析第1層。(Butterfly為第0層,三個Buffer分別是1、2、3層,最後一層是關鍵程式碼)
只需要模仿通常靜態分析手段掃過去就行了,然後按照對應OpCode生成彙編,然後再進行編譯,貼上原始碼
#include <keystone/keystone.h> ... void disassembly_vm1(vm_sub* ctx) { char* eip = ctx->vm_ins; char* esp = eip + 2 * ctx->size; ks_engine* ks; ks_err err; err = ks_open(KS_ARCH_X86, KS_MODE_32, &ks); if (err != KS_ERR_OK) { cout << "Keystone open error." << endl; return; } ostringstream dasm = ostringstream(); dasm << "push 6;" << endl; dasm << "push 0x20000;" << endl; dasm << "call vmins_0;" << endl; dasm << "jmp vmins_ret;" << endl; while (eip < ctx->vm_ins + 0x792) { int vm_offset = eip - ctx->vm_ins; dasm << "vmins_" << vm_offset << ":" << endl; int ins = *eip++; switch (ins) { case 17: { dasm << "push ebx;" << endl; break; } case 1: { uint8_t off = (uint8_t)*eip++; dasm << "xor eax, eax;" << endl; dasm << "mov al, " << (int)off << ";" << endl; dasm << "lea ebx, [ebp+eax*4-400h];" << endl; break; } case 13: { dasm << "mov ebx, [ebx];" << endl; break; } case 3: { ecx = (uint8_t)*eip++; dasm << "mov ebx, " << (int)ecx << ";" << endl; break; } case 8: { uint32_t off = *(uint32_t*)eip; dasm << "test ebx, ebx;" << endl; dasm << "jz vmins_" << (int)(vm_offset + 1 + off) << ";" << endl; dasm << "jmp vmins_" << (int)(vm_offset + 1 + 4) << ";" << endl; eip += 4; break; } case 21: { dasm << "pop ecx;" << endl; dasm << "cmp ecx, ebx;" << endl; dasm << "jnz vmins_" << vm_offset << "set0;" << endl; dasm << "mov ebx, 1;" << endl; dasm << "jmp vmins_" << vm_offset + 1 << ";" << endl; dasm << "vmins_" << vm_offset << "set0:" << endl; dasm << "mov ebx, 0;" << endl; break; } case 15: { dasm << "pop edx;" << endl; dasm << "mov [edx], ebx;" << endl; break; } case 6: { uint32_t off = *(uint32_t*)eip; //In disassembly mode we do not jump, but skip this instruction. //eip += off; if (off != 4) dasm << "jmp vmins_" << (int)(vm_offset + 1 + off) << ";" << endl; eip += 4; break; } case 29: { dasm << "pop ecx;" << endl; dasm << "add ebx, ecx;" << endl; break; } case 30: { dasm << "pop eax;" << endl; dasm << "sub eax, ebx;" << endl; dasm << "mov ebx, eax;" << endl; break; } case 14: { dasm << "xor ecx, ecx;" << endl; dasm << "mov cl, [ebx];" << endl; dasm << "mov ebx, ecx;" << endl; break; } case 31: { dasm << "pop edx;" << endl; dasm << "imul ebx, edx;" << endl; break; } case 16: { dasm << "pop eax;" << endl; dasm << "mov [eax], bl;" << endl; dasm << "movsx ebx, bl;" << endl; break; } case 33: { dasm << "pop eax;" << endl; dasm << "xor edx, edx;" << endl; dasm << "div ebx;" << endl; dasm << "mov ebx, edx;" << endl; break; } case 23: { dasm << "pop ecx;" << endl; dasm << "cmp ecx, ebx;" << endl; dasm << "jnb vmins_" << vm_offset << "set0;" << endl; dasm << "mov ebx, 1;" << endl; dasm << "jmp vmins_" << vm_offset + 1 << ";" << endl; dasm << "vmins_" << vm_offset << "set0:" << endl; dasm << "mov ebx, 0;" << endl; break; } case 32: { dasm << "pop eax;" << endl; dasm << "xor edx, edx;" << endl; dasm << "div ebx;" << endl; dasm << "mov ebx, eax;" << endl; break; } case 24: { dasm << "pop edx;" << endl; dasm << "cmp edx, ebx;" << endl; dasm << "jbe vmins_" << vm_offset << "set0;" << endl; dasm << "mov ebx, 1;" << endl; dasm << "jmp vmins_" << vm_offset + 1 << ";" << endl; dasm << "vmins_" << vm_offset << "set0:" << endl; dasm << "mov ebx, 0;" << endl; break; } case 18: { dasm << "pop ecx;" << endl; dasm << "or ebx, ecx;" << endl; break; } case 28: { dasm << "pop eax;" << endl; dasm << "mov ecx, ebx;" << endl; dasm << "shr eax, cl;" << endl; dasm << "mov ebx, eax;" << endl; break; } case 20: { dasm << "pop ecx;" << endl; dasm << "and ebx, ecx;" << endl; break; } case 19: { dasm << "pop ecx;" << endl; dasm << "xor ebx, ecx;" << endl; break; } case 27: { dasm << "pop edx;" << endl; dasm << "mov ecx, ebx;" << endl; dasm << "shl edx, cl;" << endl; dasm << "mov ebx, edx;" << endl; break; } case 22: { dasm << "pop eax;" << endl; dasm << "cmp eax, ebx;" << endl; dasm << "jz vmins_" << vm_offset << "set0;" << endl; dasm << "mov ebx, 1;" << endl; dasm << "jmp vmins_" << vm_offset + 1 << ";" << endl; dasm << "vmins_" << vm_offset << "set0:" << endl; dasm << "mov ebx, 0;" << endl; break; } case 26: { dasm << "pop ecx;" << endl; dasm << "cmp ecx, ebx;" << endl; dasm << "jb vmins_" << vm_offset << "set0;" << endl; dasm << "mov ebx, 1;" << endl; dasm << "jmp vmins_" << vm_offset + 1 << ";" << endl; dasm << "vmins_" << vm_offset << "set0:" << endl; dasm << "mov ebx, 0;" << endl; break; } case 0: { uint8_t off = (uint8_t)*eip++; //ecx = (uint32_t)&eax[4 * off]; dasm << "xor edx, edx;" << endl; dasm << "mov dl, " << (int)off << ";" << endl; dasm << "lea ebx, [ebp+edx*4];" << endl; break; } case 11: { uint32_t off = *(uint32_t*)eip; //esp += 4 * off; dasm << "mov eax, " << (int)(off * 4) << ";" << endl; dasm << "add esp, eax;" << endl; eip += 4; break; } case 4: { ecx = *(uint32_t*)eip; eip += 4; dasm << "mov ebx, " << (int)ecx << ";" << endl; break; } case 40: { //We do not execute //char* buf = (char*)*((uint32_t*)esp + 2); //uint32_t size = *(uint32_t*)esp; //ecx = (uint32_t)buf; //memset(buf, esp[4], size + (size & 3)); //eax = ebx; dasm << "mov ecx, [esp+0];" << endl; dasm << "xor eax, eax;" << endl; dasm << "mov al, [esp+4];" << endl; dasm << "mov edi, [esp+8];" << endl; dasm << "mov ebx, edi;" << endl; dasm << "rep stosb;" << endl; break; } case 42: { //We do not execute //ecx = (uint32_t) * ((uint32_t*)esp + 2); //memcpy((void*)*((uint32_t*)esp + 2), (void*)*((uint32_t*)esp + 1), *((uint32_t*)esp)); //eax = ebx; dasm << "mov ecx, [esp+0];" << endl; dasm << "mov edi, [esp+8];" << endl; dasm << "mov esi, [esp+4];" << endl; dasm << "mov ebx, edi;" << endl; dasm << "rep movsb;" << endl; break; } case 9: { uint32_t off = *(uint32_t*)eip; dasm << "test ebx, ebx;" << endl; dasm << "jz vmins_" << (int)(vm_offset + 1 + 4) << ";" << endl; dasm << "jmp vmins_" << (int)(vm_offset + 1 + off) << ";" << endl; eip += 4; break; } case 2: { uint32_t off = *(uint32_t*)eip; //ecx = (uint32_t)&eax[4 * off]; eip += 4; dasm << "mov ecx, " << (int)off << ";" << endl; dasm << "lea ebx, [ebp+ecx*4];" << endl; break; } case 7: { uint32_t off = *(uint32_t*)eip; //push(esp, (uint32_t)eip + 4); //In disassembly mode we do not jump, but skip this instruction. //eip += off; dasm << "call vmins_" << (int)(vm_offset + 1 + off) << ";" << endl; dasm << "mov ebx, eax;" << endl; eip += 4; break; } case 10: { uint32_t off = *(uint32_t*)eip; dasm << "push ebp;" << endl; dasm << "mov ebp, esp;" << endl; dasm << "sub esp, " << off * 4 << ";" << endl; eip += 4; break; } case 12: // return { dasm << "mov eax, ebx;" << endl; dasm << "mov esp, ebp;" << endl; dasm << "pop ebp;" << endl; dasm << "ret;" << endl; break; } case 43: { dasm << "mov eax, [esp];" << endl; dasm << "ret;" << endl; goto finished; } default: { cout << "Error"; break; } } } finished: dasm << "vmins_ret:" << endl; dasm << "push ebx;" << endl; dasm << "mov eax, [esp];" << endl; dasm << "ret;" << endl; unsigned char* output; size_t outlen = 0; size_t outcnt = 0; string disasm = dasm.str(); ofstream fout = ofstream("./disasm_vm1.txt", ios_base::ate); fout << disasm; fout.flush(); fout.close(); const char* code = disasm.c_str(); if (ks_asm(ks, code, 0, &output, &outlen, &outcnt) != KS_ERR_OK) { ks_err err = ks_errno(ks); cout << err; } fout = ofstream("./disasm_vm1.bin", ios_base::ate | ios_base::binary); fout.write((const char*)output, outlen); fout.flush(); fout.close(); ks_free(output); ks_close(ks); }
需要注意的是,除了第1層的40和42,以及後續層的這兩個位置的指令,其他各層都相同,因此分析後面的只需要改一下case就行了。額外,第2、3層的這兩個指令加了不少其他程式碼,但是我發現不對增加的程式碼進行增補也可以解題,後面細說。除此之外,還需要注意把lea esp的地方改為add/sub esp,不然IDA不認(非標準
分析完4層指令後,貼上關鍵的反編譯函式。
第1層
第2層
第3層
最終分析目標函式(第4層)
首先獲取了前面幾層的指令指標
這裡其實呼叫了memcpy系列函式,不過被最佳化了,由於我偷懶,並沒有為每層更改memcpy,memset系列函式的實現,因此看到這個指令,就可以認為呼叫了Host的那個地方的函式,轉而看前面層的程式碼就可以了。在這裡,經過提取分析,得到這裡memcpy對前4位元組的CRC32
int vm3_memcpy(char* dst, char* src, int len) { vm_fin* v18 = &ctx.fin; unsigned char * v17 = v18->input_hex; int* v16 = new int[10]; v16[2] = 0xDEC0CCAE; *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); *(v16 + 2) = crc32(0xFFFFFFFF, v16 + 2); if (*(v16 + 2) == 0xDE05629C) return 1; return 0; }
經過爆破,可以得出前4位元組為AE CC C0 DE
。
result = d54b1112 target = de05629c result = 5ba49ea3 target = d54b1112 result = f16f3846 target = 5ba49ea3 result = 84b4f299 target = f16f3846 result = 3731ce56 target = 84b4f299 result = 74f3e321 target = 3731ce56 result = 20558f1 target = 74f3e321 result = dec0ccae target = 20558f1 result = f812fce7 target = dec0ccae
隨後分析下一個函式
這裡修改了上層Host的指令,可以看出是修改了一些立即數(前後對照),因此在還原函式的時候稍加註意即可,對於第一個memset,提煉出關鍵校驗函式有:
int vm3_memset_1(char* dst, char val, int len) { vm_fin* v18 = &ctx.fin; char* hex = (char *)v18->input_hex; int* v16 = v18->len_buf; int* v6 = v16 + 4; *v6 = sub_E21(hex); // equals D540 v6 = v16 + 2; *v6 = sub_109A(hex + 4, (char*)v16 + 256); memset(v16 + 1024, 1, 100); sub_1517((char*)v16 + 256, (char*)v16 + 4096); return 0; }
sub_E21
完成了某種變換,可以透過爆破還原,並計算了一個值(D540)避免多解,隨後由於前4位元組已經計算出,帶偏移傳入sub_109A
unsigned int __cdecl sub_109A(char* a1, char* a2) { int v3; // [esp+3ECh] [ebp-14h] char* v4; // [esp+3F0h] [ebp-10h] unsigned __int8 v5; // [esp+3F4h] [ebp-Ch] unsigned int v6; // [esp+3F8h] [ebp-8h] unsigned int v7; // [esp+3FCh] [ebp-4h] v4 = a2; v7 = 0; v3 = 0; while (v7 < 15) { v6 = 0; v5 = a1[v7]; v3 <<= 1; v3 |= (unsigned int)v5 >> 7; while (v6 < 7) { *v4 = v5 & 1; v5 >>= 1; ++v4; ++v6; } ++v7; } return (((v3 << 8) + ((unsigned int)(unsigned __int8)a1[14] >> 2)) << 8) + (unsigned __int8)a1[15]; }
前14個位元組以及15位元組的低2位變成10*10矩陣,隨後初始化棋盤,使用sub_1517進行解密。
完成的是根據輸入,從左上角依次訪問棋盤,並對訪問位置及其相鄰的元素進行異或,最終使得全1變為全0。這裡演算法不多說,可以去看文章。完成求解
此時根據這裡的防止多解,完成前8個int的求解
AE CC C0 DE
0C
32
56
F7
5E
37
A6 BF A2
27
A2 ED
3D
54
AC
96
4B
43
54
46
32
30
32
30
46
6C
61
67
最後分析最後6個int,和前面棋盤大同小異,192位元的前190位元以三角形的方式放入矩陣,並將三角形複製8次填滿矩陣,然後求解使得棋盤翻轉。
int vm3_memset_2(char* dst, char val, int len) { vm_fin* v18 = &ctx.fin; char* v17 = (char *)v18->input_hex; int* v16 = v18->len_buf; int* v6 = v16 + 2; *v6 = sub_179A(v17 + 32, (char *)v16 + 64*4); if (*v6 == 2) { memset(v16 + 1024, 1, 1600); sub_2FB9((char*)v16 + 4*64, (char*)v16 + 4*1024); } return 0; }
*v6 == 2指的是剩餘2位元(高2位),因此求解該矩陣
uint32_t limit = pow(2, 20) - 1; for (uint32_t val = 0; val <= limit; ++val) { for (int i = 0; i < 20; ++i) { uint8_t bit = (val >> i) & 0b1; mat[0][i] = bit; mat[0][39 - i] = bit; } memset(table, 1, sizeof(table)); for (int i = 0; i < 40; ++i) { for (int k = 0; k < 40; ++k) { uint8_t bit = mat[i][k]; table[i][k] ^= bit; if (i > 0) table[i - 1][k] ^= bit; if (i < 39) table[i + 1][k] ^= bit; if (k > 0) table[i][k - 1] ^= bit; if (k < 39) table[i][k + 1] ^= bit; } if (i != 39) { for (int k = 0; k < 40; ++k) { mat[i + 1][k] = table[i][k]; } } } if (memcmp(table, truth, sizeof(truth)) == 0) { printf("Result\n"); printf("arr = []\n"); for (int i = 0; i < 40; ++i) { printf("arr.append(["); for (int k = 0; k < 40; ++k) { printf("%d%s", mat[i][k], k == 39 ? "" : ", "); } printf("]\n"); } } }
最終完成求解
拿指令碼解出來
因此最終Flag為
1 |
|
相關文章
- 2020 KCTF秋季賽 | 第二題設計及解題思路2020-11-23
- 2020 KCTF秋季賽 | 第一題點評及解題思路2020-11-20
- 2020 KCTF秋季賽 | 第四題點評及解題思路2020-11-24
- 看雪·眾安 2021 KCTF 秋季賽 | 第五題設計思路及解析2021-11-29
- 看雪·眾安 2021 KCTF 秋季賽 | 第三題設計思路及解析2021-11-22
- 看雪·眾安 2021 KCTF 秋季賽 | 第六題設計思路及解析2021-12-01
- 看雪·眾安 2021 KCTF 秋季賽 | 第七題設計思路及解析2021-12-03
- 看雪·眾安 2021 KCTF 秋季賽 | 第九題設計思路及解析2021-12-09
- 看雪·眾安 2021 KCTF 秋季賽 | 第十題設計思路及解析2021-12-16
- 看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析2021-11-25
- 看雪·眾安 2021 KCTF 秋季賽 | 第十一題設計思路及解析2021-12-15
- 看雪·深信服 2021 KCTF 春季賽 | 第五題設計思路及解析2021-05-17
- 看雪·深信服 2021 KCTF 春季賽 | 第九題設計思路及解析2021-05-28
- 看雪·深信服 2021 KCTF 春季賽 | 第六題設計思路及解析2021-05-21
- 看雪·深信服 2021 KCTF 春季賽 | 第七題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第八題設計思路及解析2021-05-25
- 看雪·深信服 2021 KCTF 春季賽 | 第四題設計思路及解析2021-05-17
- 看雪·深信服 2021 KCTF 春季賽 | 第三題設計思路及解析2021-05-14
- 看雪·深信服 2021 KCTF 春季賽 | 第二題設計思路及解析2021-05-12
- 看雪·深信服 2021 KCTF 春季賽 | 第十題設計思路及解析2021-05-31
- 看雪.紐盾 KCTF 2019 Q2 | 第五題點評及解題思路2019-07-01
- 2019 KCTF 晉級賽Q1 | 第三題點評及解題思路2019-03-28
- 2019KCTF 晉級賽Q1 | 第九題點評及解題思路2019-04-04
- 2019KCTF 晉級賽Q1 | 第十題點評及解題思路2019-04-08
- 2020年數學建模國賽B題解題思路2020-09-10
- 藍橋杯第五屆省賽題目及題解2020-10-02
- 看雪.紐盾 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
- 看雪.紐盾 KCTF 2019 Q3 | 第十題點評及解題思路2019-10-08
- 看雪.紐盾 KCTF 2019 Q2 | 第七題點評及解題思路2019-07-02
- 看雪.紐盾 KCTF 2019 Q2 | 第九題點評及解題思路2019-07-04
- 看雪.紐盾 KCTF 2019 Q2 | 第六題點評及解題思路2019-07-01
- 看雪.紐盾 KCTF 2019 Q2 | 第十題點評及解題思路2019-07-05
- 看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路2019-07-03