
Chatbot
- Đầu tiên mình sử dụng DiE để có thể check xem các thông tin cơ bản của file đề bài cung cấp.

- Check qua thì mình biết được file này là code python và sử dụng pyinstaller để pack.
- Vì thế mình sử dụng
pyinstxtractor-ngvàpylingualđể chuyển nó lại sang file python cho dễ đọc. - Dưới đây là file python mà mình đã nhận được.
import base64import jsonimport timeimport randomimport sysimport osfrom ctypes import CDLL, c_char_p, c_int, c_void_pfrom cryptography.hazmat.primitives import serialization, hashesfrom cryptography.hazmat.primitives.asymmetric import paddingimport ctypes
def get_resource_path(name): if getattr(sys, 'frozen', False): base = sys._MEIPASS else: # inserted base = os.path.dirname(__file__) return os.path.join(base, name)
def load_native_lib(name): return CDLL(get_resource_path(name))if sys.platform == 'win32': LIBNAME = 'libnative.dll'else: # inserted LIBNAME = 'libnative.so'lib = Nonecheck_integrity = Nonedecrypt_flag_file = Nonefree_mem = Nonetry: lib = load_native_lib(LIBNAME) check_integrity = lib.check_integrity check_integrity.argtypes = [c_char_p] check_integrity.restype = c_int decrypt_flag_file = lib.decrypt_flag_file decrypt_flag_file.argtypes = [c_char_p] decrypt_flag_file.restype = c_void_p free_mem = lib.free_mem free_mem.argtypes = [c_void_p] free_mem.restype = Noneexcept Exception as e: print('Warning: native lib not loaded:', e) lib = None check_integrity = None decrypt_flag_file = None free_mem = None
def run_integrity_or_exit(): if check_integrity: ok = check_integrity(sys.executable.encode()) if not ok: print('[!] Integrity failed or debugger detected. Exiting.') sys.exit(1)PUB_PEM = b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJftFGJC6RjAC54aMncA\nfjb2xXeRECiwHuz2wC6QynDd93/7XIrqTObeTpfBCSpOKRLhks6/nzZFTTsYdQCj\n4roXhWo5lFfH0OTL+164VoKnmUkQ9dppzpmV0Kpk5IQhEyuPYzJfFAlafcHdQvUo\nidkqcOPpR7hznJPEuRbPxJod34Bph/u9vePKcQQfe+/l/nn02nbfYWTuGtuEdpHq\nMkktl4WpB50/a5ZqYkW4z0zjFCY5LIPE7mpUNLrZnadBGIaLoVV2lZEBdLt6iLkV\nHXIr+xNA9ysE304T0JJ/DwM1OXb4yVrtawbFLBu9otOC+Gu0Set+8OjfQvJ+tlT/\nzQIDAQAB\n-----END PUBLIC KEY-----'public_key = Nonetry: pub_path = get_resource_path('public.pem') if os.path.exists(pub_path): with open(pub_path, 'rb') as f: public_key = serialization.load_pem_public_key(f.read()) else: # inserted public_key = serialization.load_pem_public_key(PUB_PEM)except Exception as e: print('Failed loading public key:', e) public_key = None
def b64url_encode(b): return base64.urlsafe_b64encode(b).rstrip(b'=').decode()
def b64url_decode(s): s = s | ('=', 4, len(s) - 4) | 4 return base64.urlsafe_b64decode(s.encode())
def verify_token(token): if not public_key: return (False, 'no public key') try: payload_b64, sig_b64 = token.strip().split('.', 1) payload = b64url_decode(payload_b64) sig = b64url_decode(sig_b64) public_key.verify(sig, payload, padding.PKCS1v15(), hashes.SHA256()) j = json.loads(payload.decode()) if j.get('role')!= 'VIP': return (False, 'role != VIP') if j.get('expiry', 0) < int(time.time()): return (False, 'expired') else: # inserted return (True, j) except Exception as e: return (False, str(e))
def sample_token_nonvip(): payload = json.dumps({'user': 'guest', 'expiry': int(time.time()) + 3600, 'role': 'USER'}).encode() return b64url_encode(payload)
def main(): run_integrity_or_exit() print('=== Bot Chat === \n 1.chat\n 2.showtoken\n 3.upgrade \n 4.quit') queries = 0 while True: cmd = input('> ').strip().lower() if cmd in ['quit', 'exit']: return if cmd == 'chat': if queries < 3: print(random.choice(['Hi', 'Demo AI', 'Hello!', 'How can I assist you?', 'I am a chatbot', 'What do you want?', 'Tell me more', 'Interesting', 'Go on...', 'SIUUUUUUU', 'I LOVE U', 'HACK TO LEARN NOT LEARN TO HACK'])) queries = queries | 1 else: # inserted print('Free queries exhausted. Use \'upgrade\'') else: # inserted if cmd == 'showtoken': print('Token current:' + sample_token_nonvip()) else: # inserted if cmd == 'upgrade': run_integrity_or_exit() token = input('Paste token: ').strip() ok, info = verify_token(token) if not ok: if decrypt_flag_file is None: print('Native library not available -> cannot decrypt')c else: # inserted flag_path = get_resource_path('flag.enc').encode() res_ptr = decrypt_flag_file(flag_path) if not res_ptr: print('Native failed to decrypt or error') else: # inserted flag_bytes = ctypes.string_at(res_ptr) try: flag = flag_bytes.decode(errors='ignore') except: flag = flag_bytes.decode('utf-8', errors='replace') print('=== VIP VERIFIED ===') print(flag) free_mem(res_ptr) return None print('Token invalid:', info) else: # inserted print('Unknown. Use chat/showtoken/upgrade/quit')if __name__ == '__main__': main()- Nhìn qua nhìn thấy được file này đang sử dụng đến một file lib ở bên ngoài để có thể giải mã flag nếu như việc kiểm tra token hoàn tất.
flag_path = get_resource_path('flag.enc').encode()res_ptr = decrypt_flag_file(flag_path)if not res_ptr: print('Native failed to decrypt or error')else: flag_bytes = ctypes.string_at(res_ptr) try: flag = flag_bytes.decode(errors='ignore') except: flag = flag_bytes.decode('utf-8', errors='replace') print('=== VIP VERIFIED ===') print(flag)- Và ở đoạn code này ta có thể thấy việc giải mã flag dựa hoàn toàn vào các giá trị bên trong hàm của file lib chứ không cần đến các key ở bên ngoài nhập vào.
- Vậy thì ta cùng đi phân tích file
libnative.sovà trước đấy có nhận được sau khi unpack bằngpyinstxtractor-ng. - Mình mở file bằng IDA và tìm đến hàm
decrypt_flag_file.
if ( !(unsigned int)env_checks_ok() ) return 0LL; v17 = 0LL; v1 = (void *)recover_key(&v17);- Đầu tiên thì trong hàm
decrypt_flag_filesẽ kiểm tra môi trường và khởi tạo key bằng hàmrecover_key.
if ( v17 <= 0xF || (v2 = fopen(filename, "rb"), (v3 = v2) == 0LL) ) { free(v1); return 0LL; } fseek(v2, 0LL, 2); v4 = ftell(v3); fseek(v3, 0LL, 0); if ( v4 <= 16 || (v5 = (__m128i *)malloc(v4), (v6 = v5) == 0LL) ) { fclose(v3); free(v1); return 0LL; } fread(v5, 1uLL, v4, v3); v7 = v4 - 16; fclose(v3);- Sau đó đọc ciphertext từ file chỉ định.
v8 = EVP_CIPHER_CTX_new(); if ( v17 <= 0x1F ) v9 = EVP_aes_128_cbc(); else v9 = EVP_aes_256_cbc(); if ( !(unsigned int)EVP_DecryptInit_ex(v8, v9, 0LL, v1, v18) ) { EVP_CIPHER_CTX_free(v8); goto LABEL_22; }- Sau đó chương trình sẽ chọn giải mã theo
AES-128hayAES-256tùy thuộc vào độ dài của key.
v15 = 0; v10 = (char *)malloc((int)v4); v16 = 0; if ( !(unsigned int)EVP_DecryptUpdate(v8, v10, &v15, &v6[1], v7) || !(unsigned int)EVP_DecryptFinal_ex(v8, &v10[v15], &v16) || (v11 = v15 + v16, v12 = malloc(v15 + v16 + 1), (v13 = v12) == 0LL) ) { EVP_CIPHER_CTX_free(v8); free(v6); free(v1); free(v10); return 0LL; } memcpy(v12, v10, v11); v13[v11] = 0; EVP_CIPHER_CTX_free(v8); free(v10); free(v6); free(v1); return v13;- Và cuối cùng chương trình sẽ giải mã và trả về flag.
- Dựa vào các phân tích đó cùng với dữ liệu có được trong file
flag.encđi kèm sau khi sử dụngpyinstxtractor-ngđể unpack filemainmình tiến hành viết một đoạn script python để có thể giải mã flag của bài này.
from Crypto.Cipher import AESfrom Crypto.Util.Padding import unpad
flag_enc = bytes.fromhex("C0 68 4C BE 81 D6 89 76 2C A2 40 55 FF B1 3B A9 01 95 6C 4E F3 4F 19 65 CD DC C1 12 99 37 2C BC 76 AE 48 53 61 98 5A A9 8B 8A 51 11 37 1A 57 4E 9C 87 8B 03 DC B5 3B 12 C4 7F 46 9A 0A A6 33 27")MASK = bytes([0x2A, 0x2A, 0x0A, 0x9A])OBF_KEY = bytes([ 0xEE, 0x50, 0xD1, 0xAA, 0xE0, 0x97, 0x5F, 0x43, 0xDD, 0xA8, 0xAC, 0x83, 0xF0, 0x05, 0xF3, 0xFF, 0x62, 0x08, 0xF4, 0x44, 0x4B, 0x2C, 0x55, 0xEC, 0xB9, 0x65, 0x23, 0xCC, 0x25, 0x65, 0xEE, 0x70])
def recover_key(): key = bytearray(32) key[0] = 0xC4 for i in range(1, 32): key[i] = OBF_KEY[i] ^ MASK[i & 3]
return bytes(key)
def decrypt_flag(encrypted_data, key): iv = encrypted_data[:16] ciphertext = encrypted_data[16:] cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = cipher.decrypt(ciphertext) plaintext = unpad(plaintext, AES.block_size)
return plaintext
key = recover_key()flag = decrypt_flag(flag_enc, key)print(flag.decode('utf-8', errors='ignore'))- Sau khi chạy file python thì mình đã có được flag cho bài này.
Flag
CSCV2025{reversed_vip*_chatbot_bypassed}ReezS
- Đầu tiên mình sẽ sử dụng DiE để có thể check các thông tin cơ bản của file mà đề bài cung cấp.

- Dựa vào các thông tin thì chúng ta có thể thấy đây là một file được biên dịch từ C/C++ và cũng không có gì đặc biệt lắm.
- Tiếp đến mình sử dụng IDA để xem mã code của chương trình.

- Sau khi phân tích qua code thì mình có tóm tắt như sau:
- Đầu tiên chương trình yêu cầu người dùng nhập flag vào và kiểm tra độ dài có bằng 32 ký tự hay không, nhưng ở đây mình thấy mảng
Strđược IDA khai báo chỉ 16 phần tử nên mình chỉnh lại thành 32 và đổi tênStrthànhinpđể dễ dàng phân biệt hơn.
- Tiếp đến chương trình sử dụng
_mm_xor_si128để xor với lần lượt 16 byte tronginpvớixmmword_14001E030. - Sau đó chương trình kiểm tra kết quả của
inpsau khi xor ở bước trên vớixmmword_140029000vàxmmword_140029010, nếu đúng thì sẽ in raYes, còn sai thì làNo.
- Đầu tiên chương trình yêu cầu người dùng nhập flag vào và kiểm tra độ dài có bằng 32 ký tự hay không, nhưng ở đây mình thấy mảng
- Từ tóm tắt đó thì ta sẽ kết luận được một điều là ta có thể lấy giá trị bên trong
xmmword_140029000vàxmmword_140029010ra để xor lại vớixmmword_14001E030thì ta sẽ có được flag cho bài này. - Mình tiến hành viết một đoạn script python để làm điều đó và lấy ra flag.
xmmword_140029000 = bytes.fromhex("D9C5D8D8D3F5DEC2C3D9F5C3D9F5CCCB")xmmword_140029010 = bytes.fromhex("C1CFF5CCC6CBCD8B8B8B8B8B8B8B8B8B")
print(''.join(chr(i ^ 0xAA) for i in xmmword_140029000 + xmmword_140029010))- Nhưng sau khi chạy thì kết quả lại trả về là
sorry_this_is_fake_flag!!!!!!!!!, mình vẫn cố nhập thử thì thật sự nó đúng là fake flag thật. - Nên mình lại tiếp tục mò tiếp trong hàm
mainxem có thể kiếm thêm được manh mối nào khác không. - Ở đây mình nghi ngờ
xmmword_140029000vàxmmword_140029010có thể bị thay đổi giá trị nên mình đã thử view xem thì đúng là nó đang được sử dụng ở bên trong một hàm khác.

- Như vậy thì đã rõ rồi, các giá trị đang bị thay đổi, giờ thì mình sẽ lấy các giá trị mới này thay vào trong script python ở trên và chạy lại thử xem kết quả như nào.
xmmword_140029000 = bytes.fromhex("9ACBCF9E98C9C89DC998999B9CCF9F93")xmmword_140029010 = bytes.fromhex("CFCFCF9DCF989A999B9A98CB9D9D9D9F")
print(''.join(chr(i ^ 0xAA) for i in xmmword_140029000 + xmmword_140029010))- Sau khi chạy script python ở trên và lấy kết quả nộp vào chương trình thì mình đã có được kết quả trả ra báo chính xác.
- Bọc lại với format của flag thì mình đã có được flag đầy đủ cho bài này.
Flag
CSCV2025{0ae42cb7c2316e59eee7e203102a7775}Reverse Master
- Bài này cung cấp cho một file apk nên mình sẽ sử dụng JADX để có thể xem được code của file.
- Cũng không biết tại sao khi mình chạy file apk thì không có hiện ra gì nên vì thế chưa biết được mạch chương trình chạy như nào.
- Sau khi mở file bằng JADX thì mình tìm thử
CSCV2025để xem có tìm được gì không.

- Tuyệt vời, mình có thể tìm được được phần đầu của flag luôn, cùng mở file đó ra xem như nào.
public final void onClick(android.view.View r13) { /* r12 = this; r13 = 16 r0 = 1 int r1 = com.ctf.challenge.MainActivity.b com.google.android.material.textfield.TextInputEditText r1 = com.google.android.material.textfield.TextInputEditText.this android.text.Editable r1 = r1.getText() java.lang.String r1 = java.lang.String.valueOf(r1) com.ctf.challenge.MainActivity r2 = r2 java.lang.String r3 = "CSCV2025{" boolean r3 = r1.startsWith(r3) r4 = 0 if (r3 != 0) goto L1c L1a: r13 = r4 goto L6e L1c: java.lang.String r3 = "}" boolean r3 = r1.endsWith(r3) if (r3 != 0) goto L25 goto L1a L25: int r3 = r1.length() int r3 = r3 - r0 r5 = 9 java.lang.String r1 = r1.substring(r5, r3) java.lang.String r3 = "substring(...)" o.F2.e(r1, r3) java.lang.String r5 = r1.substring(r4, r13) o.F2.e(r5, r3) byte[] r6 = new byte[r13] r6 = {x0090: FILL_ARRAY_DATA , data: [122, 86, 27, 22, 53, 35, 80, 77, 24, 98, 122, 7, 72, 21, 98, 114} // fill-array byte[] r7 = new byte[r13] r8 = r4 L44: if (r8 >= r13) goto L55 r9 = r6[r8] byte[] r10 = r2.a int r11 = r10.length int r11 = r8 % r11 r10 = r10[r11] r9 = r9 ^ r10 byte r9 = (byte) r9 r7[r8] = r9 int r8 = r8 + r0 goto L44 L55: java.lang.String r6 = new java.lang.String java.nio.charset.Charset r8 = o.X.a r6.<init>(r7, r8) boolean r5 = r5.equals(r6) if (r5 != 0) goto L63 goto L1a L63: java.lang.String r13 = r1.substring(r13) o.F2.e(r13, r3) boolean r13 = r2.checkSecondHalf(r13) L6e: com.google.android.material.textfield.TextInputLayout r1 = r3 if (r13 == 0) goto L80 java.lang.String r13 = "🎉 Correct! Flag is valid!" android.widget.Toast r13 = android.widget.Toast.makeText(r2, r13, r0) r13.show() r13 = 0 r1.setError(r13) return L80: java.lang.String r13 = "❌ Wrong flag! Try again!" android.widget.Toast r13 = android.widget.Toast.makeText(r2, r13, r4) r13.show() java.lang.String r13 = "Invalid flag" r1.setError(r13) return */ throw new UnsupportedOperationException("Method not decompiled: o.ViewOnClickListenerC0153y3.onClick(android.view.View):void");}- Ở bên trong file đó, mình thấy có một đoạn code đã bị comment lại và khi nhìn qua thì có vẻ như nó chính là đoạn code check đoạn đầu của flag.
- Cùng dịch nó sang python để dễ nhìn hơn.
def onClick(flag: str): key = [66, 51, 122, 33, 86] r6 = [122, 86, 27, 22, 53, 35, 80, 77, 24, 98, 122, 7, 72, 21, 98, 114]
if not (flag.startswith("CSCV2025{") and flag.endswith("}")): print("❌ Wrong flag! Try again!") return
inner = flag[9:-1] first_half = inner[:16] second_half = inner[16:]
r7 = [] for i in range(16): r7.append(r6[i] ^ key[i % len(key)])
expected_first_half = bytes(r7).decode("utf-8", errors="replace")
if first_half != expected_first_half: print("❌ Wrong flag! Try again!") return
if checkSecondHalf(second_half): print("🎉 Correct! Flag is valid!") else: print("❌ Wrong flag! Try again!")- Ta có thể thấy nó đang xor
keyvớir6sau đó so sánh với 16 ký tự đầu ở trong flag, nếu đúng là sẽ check đến 16 ký tự sau. - Nhưng mà phần check sau ở đâu?
static { try { System.loadLibrary("native-lib"); } catch (UnsatisfiedLinkError e) { Log.e("CTF", "❌ Native lib failed: " + e.getMessage()); }}- Thì ở phần này ta thấy chương trình load thêm file
native-lib, ta tiến hành export filelibnative-lib.soở trong lib ra để có thể xem được file này liệu đang làm gì.

- Sau khi phân tích qua thì mình thấy được hàm
sub_1AD68chính là hàm xử lý logic chính để có thể check các ký tự của flag nhập vào. - Cùng phân tích xem hàm đó đang làm gì.
- Nhìn qua thì hàm này đã bị làm rối khiến nó nhìn khá dài và rắc rối nhưng thật ra nó luôn chỉ hoạt động theo một hướng.

- Đầu tiên nó khởi tạo một mảng v5.

- Sau đó khởi tạo các giá trị cho mảng v3.

- Cuối cùng là dùng các giá trị trong v3 đó để xor với input nhập vào để kiểm tra và trả ra kết quả.
- Sau đây là script python của mình để có thể giải ngược lại flag cho bài này.
def first_half(): key = [66, 51, 122, 33, 86] first = [122, 86, 27, 22, 53, 35, 80, 77, 24, 98, 122, 7, 72, 21, 98, 114] return "".join([chr(first[i] ^ key[i % len(key)]) for i in range(16)])
def second_half(): v5 = [0x7D, 0xE2, 0x14, 0xB8, 0x63] v7 = 0x63 v8 = 0x7D v9 = 0xE2 v10 = 0x14 v11 = 0xB8
v15 = v9 v16 = v10 | 1 v17 = [ v8 ^ 4, v9 | 5, v10 ^ 6, v11 | 7, v7 | 8, v8 ^ 9, v9 ^ 0xA, v10 | 0xB ] v18 = (v7 ^ 0x74) - 19 v19 = v7 ^ 0xD v20 = v5[1] v21 = v5[0] v22 = v5[2] v23 = v5[3] v24 = v5[4]
v3 = [0]*16 v3[0] = ((v8 ^ 0x2F) - 7) ^ v21 v3[1] = ((v15 ^ 0x6C) - 10) ^ v20 v3[2] = ((v16 ^ 0x95) - 13) ^ v22 ^ 2 v3[3] = (((v11 | 2) ^ 0x21) - 16) ^ v23 v3[4] = v18 ^ v24 ^ 4 v3[13] = ((v11 ^ 8) - 46) ^ v23 ^ 0xD v3[14] = ((v19 ^ 0x57) - 49) ^ v24 ^ 0xE v3[15] = ((v8 ^ 7) - 52) ^ v21 ^ 0xF
def to_bytes_le(val): return [(val >> (8*i)) & 0xFF for i in range(8)]
b1 = to_bytes_le(0x53E81E454D2E4748) b2 = to_bytes_le(0xD5D8DBDEE1E4E7EA) b3 = to_bytes_le(0x0C0B0A0908070605)
v6 = [v21, v20, v22, v23, v24, 0, 0, 0]
tmp1 = [ v17[i] ^ b1[i] for i in range(8) ] tmp2 = [ (tmp1[i] + b2[i]) & 0xFF for i in range(8) ] tmp3 = [ tmp2[i] ^ b3[i] for i in range(8) ]
table_val = 0x0201000403020100 tbl = to_bytes_le(table_val)
vqtbl = [ v6[tbl[i]] if tbl[i] < 8 else 0 for i in range(8) ] res = [ tmp3[i] ^ vqtbl[i] for i in range(8) ] for i in range(8): v3[5 + i] = res[i]
return "".join(chr(i) for i in v3)
print("CSCV2025{" + first_half() + second_half() + "}")- Sau khi chạy code thì mình đã có flag cho bài này.
Flag
CSCV2025{8ea7cac7948424406fe3ccc3cf2197e4}