Dưới đây là toàn bộ writeup “có tâm” cho tất cả các bài pwnable được mình giải trong/sau cuộc thi. Các challenge khác mình sẽ cập nhật thêm nếu có thời gian làm thử.
Writeup “có tâm” nghĩa là mình sẽ trình bày chi tiết nhất từng thao tác xử lý bài toán, từ việc kiểm tra thông tin file, rename các biến, tạo struct, … Nếu các bạn đã có kinh nghiệm, vui lòng xem #Final script để đỡ tốn thời gian.
Mình nghĩ 4 bài pwnable hoàn toàn giải được trong 8 tiếng, nhưng 3 bài đầu tiên có số solve khá nhiều: 80 - 54 - 33 thì mình cũng hơi bất ngờ. Hoặc là có gian lận hoặc là các bạn sinh viên chơi pwn càng ngày càng khủng (hy vọng là như vậy). Dù sao thì mình cũng chỉ quan tâm tới bản thân, các thí sinh khác chơi như nào thì đó là lựa chọn riêng của họ 😑
pwn/RacehorseS
0x00 TL;DR
Hàm main() có Format String Bug (FSB) → Arbitrary Read/Write (AAR/AAW).
AAW ghi đè exit@got thành main_addr để chương trình luôn được thực thi mà không bị gọi exit.
AAR để leak địa chỉ Libc và Stack.
AAW ghi đè strlen@got thành system_addr.
Khi hàm main() được gọi lại, strlen(input) sẽ trở thành system("/bin/sh").
0x01 Building the environment & Patch binary
Build và chạy Docker. Do server dùng pwn.red/jail:0.3.0 nên port luôn được expose ra 5000.
Copy libc và ld trong Docker ra ngoài local, dùng pwninit để patch binary.
1
2
3
4
5
6
7
8
9
10
11
12
13
➜ pwn-RacehorseS cd ./bin
➜ bin docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
99825ddf5f95 racehorses:latest "/jail/run"19 seconds ago Up 19 seconds 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp sad_pare
➜ bin docker cp 99825ddf5f95:/srv/usr/lib/x86_64-linux-gnu/libc.so.6 .
➜ bin docker cp 99825ddf5f95:/srv/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 .
➜ bin pwninit
bin: ./horse_say
libc: ./libc.so.6
ld: ./ld-linux-x86-64.so.2
copying ./horse_say to ./horse_say_patched
running patchelf on ./horse_say_patched
0x02 Overview
Binary không bị strip, lớp bảo vệ PIE tắt, Partial RelRO nên chúng ta có thể overwrite bảng GOT.
1
2
3
4
5
6
7
8
9
➜ bin file horse_say
horse_say: ELF 64-bit LSB executable, x86-64, version 1(SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7b5c31c696700b3cb0d434cd475b001e860e26c4, for GNU/Linux 3.2.0, not stripped
gef➤ checksec
[+] checksec for'/home/shilong/ctfs/ascis/pwn-RacehorseS/bin/horse_say'Canary : ✓
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
Chương trình cho phép nhập input, in ra màn hình chuỗi vừa nhập và kết thúc.
1
2
3
4
5
6
7
8
9
10
➜ bin ./horse_say
Say something: abcdef
________
< abcdef >
--------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||-----||||||
Nhiệm vụ trước tiên là phải làm cho chương trình được thực thi liên tục mà không bị gọi exit(). Do PIE tắt nên địa chỉ hàm main() tĩnh (không thay đổi), ta sẽ dùng FSB để ghi đè exit@got thành main_addr. Khi đó chương trình sẽ liên tục được lặp lại.
Kế hoạch khai thác tiếp theo được thiết kế như sau:
Stack layout của sau khi nhập payload1 phía trên như sau
Giải thích ý nghĩa payload:
rsp+0x30 chứa nội dung payload, rsp+0x50 chứa exit@got là địa chỉ của nội dung cần overwrite. Ta thấy chỉ cần overwrite 2 byte cuối 10c0 thành 12d9, vậy nên sẽ dùng format string $hn.
Tính từ đỉnh stack rsp (index = 1), địa chỉ rsp+0x50 nằm ở index = 0x50 / 8 + 1= 11. Đối với binary 64 bit, để ghi giá trị cho index i, ta sẽ dùng %{i+5} do phải ghi cho 5 thanh ghi mặc định trước đã.
Ta thấy phải ghi tổng cộng 4 byte cho strlen_addr từ 553b6cc0 → 5535a750. Mình sẽ chia nhỏ làm 2 phần để ghi 0xa750 và 0x5535 bởi vì nếu khi 1 lúc 4 byte 0x5535a750 thì chương trình sẽ phải chạy rất lâu.
Do $hn sẽ ghi số byte được in ra được trước đó vào địa chỉ cụ thể. Vậy nên mình phải sort 2 giá trị trên để xem phải ghi giá trị nào trước.
Lần đầu sẽ ghi 0x5535 byte.
Lần sau sẽ ghi: 0xa750 - 0x5535 = 0x521b byte.
Stack layout sau khi gửi payload3 như sau
Phân tích payload:
Do 0x5535 < 0xa750 nên sẽ ghi 0x5535 (21813) byte vào strlen@got + 2 trước.
Ghi số byte còn lại: 0xa750 - 0x5535 = 0x521b (21019) byte vào strlen@got sau.
➜ challenge file challenge_patched
challenge_patched: ELF 64-bit LSB executable, x86-64, version 1(SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=bed4c09ebd8a7a1951c09067975d3c81c2ed4c93, not stripped
gef➤ checksec
[+] checksec for'/home/ducdatdau/ctfs/2025/ascis/pwn-heapnote/heapnote/challenge/challenge_patched'Canary : ✓ (value: 0x4dc21d59748d6b00)NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
➜ challenge ./ld-linux-x86-64.so.2 ./libc.so.6
GNU C Library (Ubuntu GLIBC 2.39-0ubuntu8.6) stable release version 2.39.
Copyright (C)2024 Free Software Foundation, Inc.
This is free software; see the sourcefor copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 13.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
Minimum supported kernel: 3.2.0
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
Sau khi phân tích qua mã giả chương trình, ta biết được mỗi lần create_note() sẽ allocate ra một chunk có size 0x30. Chức năng read_note() và write_note() cho phép đọc/ghi nội dung của chunk.
Để dễ phân tích, mình đã tạo một struct mới cho chunk với cấu trúc như sau:
Việc tạo struct cho các bài heap luôn được khuyến khích làm đầu tiên. Nếu chưa đủ kinh nghiệm để phân tích thành phần struct, các bạn có thể nhờ AI làm hộ.
Vào tab Local Types → right click vào bảng list struct → Add type …
Ép kiểu cho g_note từ __int64 thành note* (ấn phím y)
intcreate_note(){note*curr_note;// [rsp+0h] [rbp-10h]
structnote*new_note;// [rsp+8h] [rbp-8h]
if(g_note){for(curr_note=g_note;curr_note->next;curr_note=curr_note->next);new_note=(structnote*)malloc(0x30u);new_note->index=curr_note->index+1;new_note->next=0;curr_note->next=new_note;returnprintf("Note with index %u created\n",new_note->index);}else{g_note=(note*)malloc(0x30u);g_note->index=0;g_note->next=0;returnputs("Note with index 0 created");}}
Dễ thấy hàm write_note() có bug Heap Overflow (HOF)
1
gets(curr_note->data);
Từ bug HOF, ta có thể ghi đè được toàn bộ nội dung của các chunk liền kề. Logic của đoạn code tìm chunk[idx] rất đơn giản, nó sẽ so sánh curr_note->index với idx vừa được nhập vào, nếu khác nhau sẽ chuyển sang chunk kế tiếp. Quá trình tìm kiếm sẽ kết thúc khi tìm tới chunk cuối cùng, nghĩa là curr_note->next = NULL.
Vấn đề xảy ra đó là byte đầu tiên của chunk→data là \x00 (địa chỉ 0x404018) nên các byte còn lại sẽ không được in ra. Để giải quyết vấn đề này, mình đưa curr_note trỏ về 0x404009, khi đó idx = 0x4010.
Tính toán tương tự như Stage 1, ta sẽ overwrite gets@got thành system_addr, đồng thời đưa chuỗi /bin/sh lên nội dung của chunk 0. Khi chọn write_note() cho chunk 0, gets(input) = system("/bin/sh").
Chúng ta được nhập 3 giá trị: row, col và value vào bảng Sudoku. Để kiểm tra ô đó hợp lệ hay không, chương trình sẽ check bởi 2 hàm:
canEdit(): ô ở hàng row, cột col không nằm trong danh sách các ô được đặt giá trị mặc định từ trước.
isValid(): kiểm tra giá trị value đã tồn tại ở bảng 3x3 và bảng 9x9 hay chưa.
Nếu vượt qua được 2 hàm check phía trên thì sẽ ghi value vào hàng row cột col.
Mình nhận ra cho dù có chơi thắng game thì vẫn không có flag hay shell. Từ đó phải đi tìm bug, khai thác bug mới lấy được shell.
0x02 Finding the bug
Bug #1: Buffer Overflow (BOF)
Chương trình cho nhập 39 byte vào mảng input[] 28 byte dẫn tới bug BOF.
1
2
printf("What's your name? ");v5=read(0,input,39u);// BUG: BOF
Bug #2: Out Of Bound (OOB)
Ta thấy 2 hàm check canEdit() và isValid() không kiểm tra phạm vi của row và col, dẫn đến bug OOB, vì vậy ta có thể tùy ý ghi giá trị value vào vùng nhớ BOARD[9 * row + col].
0x03 Building the payload
Ta đang có privimitive AAW, BOARD lại nằm trên ở vùng nhớ bss nên ta hoàn toàn đưa được shellcode lên một vùng nhớ có quyền rwx.
Việc tiếp theo là đưa chương trình sau khi kết thúc trỏ về shellcode. Ta sẽ khai thác bug#1 BOF để làm việc này. Do input[] chiếm 28 byte, ta chỉ còn viết được 39 - 28 = 11 (bytes), không đủ để overwrite ret_addr mà chỉ ghi được saved_rbp. Vì vậy, ngoài việc viết shellcode, mình còn phải dùng AAW để setup cho công việc Stack Pivot.
Memory layout của BOARD và ORIGINAL như hình vẽ. Ta thấy được các ô nhớ được set giá trị mặc định trong BOARD sẽ có giá trị 1 ở ORIGINAL. Vậy mình chỉ ghi được các ô có giá trị 0 trong bảng ORIGINAL.
Do vùng ghi shellcode không liên quan gì tới bảng BOARD nên không cần quan tâm tới nó.
Công thức Stack Pivot để đưa RIP về địa chỉ $x$ là:
saved_rbp = $k$ - 8
[k] = $x$
$[x] = y$ nghĩa là giá trị tại địa chỉ $x$ là $y$.
Stack Pivot là dùng cặp lệnh leave + ret để đưa RIP về một địa chỉ tùy ý.
Giải thích: Giả sử saved_rbp = $x$, sau lệnh leave:
RBP = [saves_rbp] = $[x]$
RSP = $x$ + 8
Sau lệnh ret:
RIP = [RSP] = [$x$ + 8]
RSP = RSP + 8 = ($x$ + 8) + 8 = $x$ + 16
Kết hợp cấu trúc bộ nhớ và công thức Stack Pivot, ta sẽ setup như sau:
unsigned__int64start_quiz_challenge(){intquestion_count_limit;// eax
__int64correct_answer_count;// [rsp+0h] [rbp-1C0h]
intquestion_index;// [rsp+8h] [rbp-1B8h]
intoption_index;// [rsp+Ch] [rbp-1B4h]
intnum_questions_to_ask;// [rsp+10h] [rbp-1B0h]
ssize_tbytes_read;// [rsp+18h] [rbp-1A8h]
_DWORDshuffled_question_indices[50];// [rsp+20h] [rbp-1A0h] BYREF
charanswer_buffer[8];// [rsp+E8h] [rbp-D8h] BYREF
charbuf[200];// [rsp+F0h] [rbp-D0h] BYREF
unsigned__int64stack_canary;// [rsp+1B8h] [rbp-8h]
stack_canary=__readfsqword(0x28u);if(g_is_player_created){timeout_handler();puts("\n=== QUIZ ON THE HANOI CONVENTION ON INFORMATION SECURITY ===");printf("Welcome %s to the cybersecurity knowledge quiz.\n",g_player_name);puts("Answer the questions correctly to get bonus points and level up!");for(correct_answer_count=0;SHIDWORD(correct_answer_count)<g_question_count;++HIDWORD(correct_answer_count))shuffled_question_indices[SHIDWORD(correct_answer_count)]=HIDWORD(correct_answer_count);shuffle_array((__int64)shuffled_question_indices,g_question_count);question_count_limit=g_question_count;if(g_question_count>10)question_count_limit=10;num_questions_to_ask=question_count_limit;for(question_index=0;question_index<num_questions_to_ask;++question_index){printf("\n--- Question %d ---\n",question_index+1);puts((constchar*)g_question_bank+772*shuffled_question_indices[question_index]);for(option_index=0;option_index<=3;++option_index)puts((constchar*)g_question_bank+772*shuffled_question_indices[question_index]+128*option_index+256);printf(dword_55555555792F);if(!fgets(answer_buffer,8,stdin))returnstack_canary-__readfsqword(0x28u);if(atoi(answer_buffer)==*((_DWORD*)g_question_bank+193*shuffled_question_indices[question_index]+192)){puts("Correct! You are very knowledgeable about information security.");LODWORD(correct_answer_count)=correct_answer_count+1;g_player_score+=10;}else{g_player_score-=10;printf("Wrong! You lose 10 points. Remaining: %u\n",g_player_score);}usleep(0xF4240u);}puts("\n=== END OF QUIZ ===");printf("You answered %d/%d questions correctly.\n",correct_answer_count,num_questions_to_ask);printf("Current score: %u\n",g_player_score);if((int)correct_answer_count<num_questions_to_ask){puts("\nYou need to try harder next time to master the rules.");}else{puts("\nCONGRATULATIONS! You passed the quiz with an excellent result!");++g_quizzes_passed_current_rank;++g_total_quizzes_passed;if(g_quizzes_passed_current_rank>=g_player_rank){g_quizzes_passed_current_rank=0;printf(asc_555555557A38,(unsignedint)++g_player_rank);}if(g_player_rank<=19||(unsignedint)g_player_score<=1999){snprintf(g_player_activity_log,0x40u,&byte_555555557B10,(unsignedint)g_player_rank,(unsignedint)g_player_score,(unsignedint)(g_player_rank-g_quizzes_passed_current_rank),correct_answer_count);}else{puts("\nYou have shown deep understanding and are awarded an honorary certificate!");printf("Write your thoughts: ");bytes_read=read(0,buf,224u);// [BUG] Buffer Overflow
if(bytes_read>0){if(buf[bytes_read-1]==10)buf[bytes_read-1]=0;elsebuf[bytes_read]=0;printf("Added to log: %s\n",buf);snprintf(g_player_activity_log,0x40u,"You have reached rank %d\nYour thoughts: %s",g_player_rank,buf);}}}}else{puts("No player yet! Please create a character first.");}returnstack_canary-__readfsqword(0x28u);}
unsigned__int64create_new_player(){ssize_tbytes_read;// [rsp+8h] [rbp-58h]
charname_buffer[72];// [rsp+10h] [rbp-50h] BYREF
unsigned__int64stack_canary;// [rsp+58h] [rbp-8h]
stack_canary=__readfsqword(0x28u);timeout_handler();printf("Enter your name: ");bytes_read=read(0,name_buffer,0x40u);if(bytes_read>0){if(name_buffer[bytes_read-1]==10)name_buffer[bytes_read-1]=0;g_player_score=100;g_quizzes_passed_current_rank=0;g_player_rank=1;g_total_quizzes_passed=0;strncpy(g_player_name,name_buffer,0x40u);strcpy(g_player_activity_log,"You are ready for the knowledge challenge.");g_player_welcome_message=(&g_welcome_messages_array)[rand()%8];g_is_player_created=1;printf("Welcome, %s!\n",g_player_name);}returnstack_canary-__readfsqword(0x28u);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
intview_player_info(){if(!g_is_player_created)returnputs("No player yet! Please create a character first.");timeout_handler();puts("\n=== Player Information ===");printf("Name: %s\n",g_player_name);printf("Score: %u\n",g_player_score);printf("Quizzes Passed: %d\n",g_quizzes_passed_current_rank);printf("Rank: %d\n",g_player_rank);printf("Activity Log: ");__printf_chk(1,g_player_activity_log);putchar(10);returnputs(g_player_welcome_message);}
unsigned__int64edit_player_name(){size_tv1;// [rsp+8h] [rbp-98h]
charnew_name_buffer[136];// [rsp+10h] [rbp-90h] BYREF
unsigned__int64stack_canary;// [rsp+98h] [rbp-8h]
stack_canary=__readfsqword(0x28u);if(g_is_player_created){if(g_player_rank>4){timeout_handler();printf("Enter new name: ");if(fgets(new_name_buffer,128,stdin)){v1=strlen(new_name_buffer);if(v1&&new_name_buffer[v1-1]==10)new_name_buffer[v1-1]=0;strcpy(g_player_name,new_name_buffer);// [BUG] Buffer Overflow; sizeof(g_player_name) = 64
puts("Player information updated!");}}else{puts("You need to reach rank 5 to edit player information!");}}else{puts("No player yet! Please create a character first.");}returnstack_canary-__readfsqword(0x28u);}
0x02 Crawl all questions
Để chơi được challenge này, mình phải crawl được toàn bộ câu hỏi và đáp án đúng của nó thì mới có thể lên rank được. Công việc này mình đã nhờ ChatGPT code hộ và lấy được tổng cộng 48 bộ đề. Như này là đủ để mình có thể trả lời đúng toàn bộ câu hỏi.
#!/usr/bin/env python3frompwnimport*importtimeimporthashlibimportjsonimportrandomimportosimportreHOST="pwn4.cscv.vn"PORT=9999KNOWLEDGE_FILE="questions.json"PLAYER_NAME=b"ducdatdau"TOTAL_QUESTIONS=50defproof_of_work(p,prefix='000000'):p.recvuntil(b"Challenge: ")challenge_string=p.recvline().strip().decode()log.info(f"Đang giải PoW cho challenge: {challenge_string}")nonce=0whileTrue:x_str=str(nonce)test_string=challenge_string+x_strtest_bytes=test_string.encode('utf-8')hash_obj=hashlib.sha256(test_bytes)hash_hex=hash_obj.hexdigest()ifhash_hex.startswith(prefix):log.success(f"Tìm thấy X = {x_str}")returnx_strnonce+=1ifnonce%1000000==0:log.info(f"Testing {nonce} hash...")defconnect_and_login():try:p=remote(HOST,PORT)ans=proof_of_work(p)p.sendlineafter(b"answer: ",ans.encode())p.sendlineafter(b"> ",b"1")p.sendlineafter(b"name: ",PLAYER_NAME)p.sendlineafter(b"> ",b"3")returnpexceptExceptionase:log.error(f"Lỗi trong quá trình kết nối hoặc PoW/Login: {e}")if'p'inlocals()andp:p.close()returnNonedefload_knowledge_base(filename):ifnotos.path.exists(filename):return{}try:withopen(filename,'r',encoding='utf-8')asf:db=json.load(f)exceptjson.JSONDecodeError:log.warning(f"Lỗi đọc file {filename}. Tạo cơ sở dữ liệu mới.")return{}ifnotdb:return{}first_key=list(db.keys())[0]ifisinstance(db.get(first_key),dict):first_entry=db[first_key]needs_migration="correct_answer"infirst_entryor"incorrect_answers"infirst_entryifneeds_migration:log.info("Phát hiện 'bộ não' phiên bản cũ. Đang di chuyển...")migrated_db={}forq_text,entryindb.items():migrated_db[q_text]={"question":entry.get("question",q_text),"options":entry.get("options",[]),"correct_option":None,"incorrect_answers_text":entry.get("incorrect_answers",[])}try:backup_name=filename+'.old_backup'os.rename(filename,backup_name)log.info(f"Đã sao lưu 'bộ não' cũ sang {backup_name}")exceptOSErrorase:log.warning(f"Không thể sao lưu file cũ: {e}. Ghi đè.")save_knowledge_base(migrated_db,filename)log.info("Di chuyển hoàn tất.")returnmigrated_dbreturndbdefsave_knowledge_base(db,filename):"""Lưu 'bộ não' vào file JSON."""try:withopen(filename,'w',encoding='utf-8')asf:json.dump(db,f,indent=4,ensure_ascii=False)exceptExceptionase:log.error(f"Không thể lưu 'bộ não' vào {filename}: {e}")defchoose_answer(db,question_text,current_options):ifquestion_textnotindb:db[question_text]={"question":question_text,"options":current_options,"correct_option":None,"incorrect_answers_text":[]}entry=db[question_text]ifentry["correct_option"]:correct_index_int=int(entry["correct_option"])if1<=correct_index_int<=len(current_options):chosen_num_str=str(correct_index_int)chosen_text=current_options[correct_index_int-1]returnchosen_num_str,chosen_textelse:log.warning(f"Index đúng ({correct_index_int}) không hợp lệ! Đặt lại = None.")entry["correct_option"]=Nonebad_texts=entry["incorrect_answers_text"]possible_options=[(str(i+1),text)fori,textinenumerate(current_options)iftextnotinbad_texts]ifnotpossible_options:log.warning(f"Đã loại trừ hết đáp án cho câu: '{question_text}'. Reset và thử lại...")entry["incorrect_answers_text"]=[]possible_options=[(str(i+1),text)fori,textinenumerate(current_options)]returnrandom.choice(possible_options)defupdate_knowledge(db,question_text,chosen_num_str,chosen_text,is_correct):ifquestion_textnotindb:returnentry=db[question_text]ifis_correct:entry["correct_option"]=int(chosen_num_str)entry["incorrect_answers_text"]=[]else:ifchosen_textnotinentry["incorrect_answers_text"]:entry["incorrect_answers_text"].append(chosen_text)defclean_text(byte_str):text=byte_str.decode('utf-8',errors='ignore').strip()returnre.sub(r'^\d+\.\s*','',text)defsolve_quiz_attempt(p,knowledge_db):try:p.recvuntil(b"level up!\n\n")foriinrange(TOTAL_QUESTIONS):q_num_in_batch=(i%10)+1ifi>0andq_num_in_batch==1:try:log.info("Hoàn thành batch. Chờ LEVEL UP! và Main Menu...")p.recvuntil(b"LEVEL UP!",timeout=5)p.recvuntil(b"> ",timeout=5)# Chờ dấu nhắc Main Menulog.info("Đang chọn '3. Start Challenge' cho batch tiếp theo...")p.sendline(b"3")p.recvuntil(b"level up!\n\n",timeout=5)exceptEOFError:log.warning("Server ngắt kết nối khi đang chờ menu sau khi level up.")return'FAIL'exceptExceptionase:log.warning(f"Lỗi khi chờ menu: {e}. Có thể server không như mong đợi.")return'FAIL'try:p.recvuntil(f"--- Question {q_num_in_batch} ---".encode(),timeout=10)p.recvline()# \nquestion_text=clean_text(p.recvline())options=[clean_text(p.recvline())for_inrange(4)]p.recvuntil(b"> ",timeout=5)exceptEOFError:log.warning(f"Server ngắt kết nối khi đang chờ câu hỏi {i+1} (batch Q{q_num_in_batch}).")return'FAIL'ifnotquestion_textorlen(options)!=4:log.error(f"Lỗi phân tích câu hỏi/lựa chọn: {question_text} | {options}")return'FAIL'chosen_num_str,chosen_text=choose_answer(knowledge_db,question_text,options)p.sendline(chosen_num_str.encode())log.info(f"[Câu {i+1}/50] (Batch Q{q_num_in_batch}) Chọn: {chosen_text}")result_bytes=p.recvline()ifb'You need to try harder'inresult_bytes:log.warning("Thất bại! Server báo 'You need to try harder'.")update_knowledge(knowledge_db,question_text,chosen_num_str,chosen_text,False)# Vẫn họcreturn'FAIL'is_correct=b"Correct!"inresult_bytesifis_correct:log.success(f"[Câu {i+1}/50] ==> ĐÚNG")else:log.warning(f"[Câu {i+1}/50] ==> SAI. (Server: {result_bytes.decode().strip()})")update_knowledge(knowledge_db,question_text,chosen_num_str,chosen_text,is_correct)save_knowledge_base(knowledge_db,KNOWLEDGE_FILE)# Lưu ngay lập tứclog.success("\n--- HOÀN THÀNH TẤT CẢ 50 CÂU HỎI (5 BATCH) ---")return'SUCCESS'exceptEOFError:log.error("\n[LỖI] Server đã đóng kết nối đột ngột.")return'FAIL'exceptExceptionase:log.error(f"\n[LỖI] Đã xảy ra lỗi không xác định: {e}")return'FAIL'finally:save_knowledge_base(knowledge_db,KNOWLEDGE_FILE)defmain():knowledge_db=load_knowledge_base(KNOWLEDGE_FILE)whileTrue:correct_count=len([eforeinknowledge_db.values()ife.get("correct_option")isnotNone])log.info(f"Database: {correct_count} correct ans.")p=connect_and_login()ifp:result=solve_quiz_attempt(p,knowledge_db)ifresult=='SUCCESS':breakifresult=='FAIL':log.warning("Lượt chạy thất bại. Chờ 2 giây và thử lại...")ifp:p.close()time.sleep(2)else:log.error("Kết nối hoặc đăng nhập thất bại. Chờ 5 giây và thử lại...")time.sleep(5)knowledge_db=load_knowledge_base(KNOWLEDGE_FILE)if__name__=="__main__":main()
Bộ câu hỏi mình lấy được lưu ở questions.json
0x03 Finding the bugs
Chương trình có khá nhiều bug nhưng mình đã không tận dụng hết toàn bộ bug tìm ra được vẫn có thể lấy được shell. Một điều nữa mình không chắc chắn là hàm load_questions_from_json() mình không chắc là có bug hay không.
Bug #1: Buffer Overflow in start_quiz_challenge()
Nếu g_player_rank > 19 và g_player_score > 2000 thì bug BOF được kích hoạt khi cho đọc vào buf[] 224 byte mặc dù kích thước của nó chỉ 200 byte.
Bug #2: Buffer Overflow in edit_player_name()
Chức năng edit_player_name() có bug BOF vì đã copy trực tiếp new_name_buffer có kích thước 128 byte vào g_player_name có size 64 byte.
Do g_player_name nằm trên bss kề với các biến khác nên có thể tận dụng bug này để ghi đè giá trị cho các biến quan trọng như g_player_score, g_player_rank, …
Bug #3: Memory leak
Do name_buffer nằm trên stack, được tái sử dụng mà không memset nên có thể leak được những dữ liệu có ích.