ReezS
When I take the very first look at this program i was just thinking this must be a basic flag_checker program


so I basically just wrote a script in order to get the flag but all I got is sorry_this_is_fake_flag!!!!!!!!!
I took me more than 4 hours finding sussy stuff in this program. Fortunately, I realized that when I use debugger to run the program with key sorry_this_is_fake_flag!!!!!!!!!, it always returns Yes but when I run the program in my shell with the same key, it returns No. Despite of seeing that, I didn’t do anything but just tried to figure out the program flow (I’m blind tho)
After the contest, thanks to an anti-debug challenge, I came up with the idea to check the import page which shows all function are imported into the program

Here we can see that IsDebuggerPresent is imported
As I thought, IsDebuggerPresent is called when starting the program to check if it is being run by a debugger or not


Thus, the actual encoded flag are used to check our input when we use debugger so we just replace it in script and get the real flag hehe
This is my script:
1
2
3
4
5
6
7
8
9
10
11
12
| def xor_bytes(a: bytes, b: bytes) -> bytes:
return bytes(x ^ y for x, y in zip(a, b))
def main():
factor0 = bytes.fromhex('A' * 32)
factor1 = bytes.fromhex('939FCF9C9B9998C99DC8C9989ECFCB9A')
factor2 = bytes.fromhex('9F9D9D9DCB989A9B999A98CF9DCFCFCF')
part1 = xor_bytes(factor1, factor0)
part2 = xor_bytes(factor2, factor0)
flag_bytes = part2 + part1
print(f"CSCV2025{{{flag_bytes.decode('utf-8')[::-1]}}}")
if __name__ == '__main__':
main()
|
CSCV2025{0ae42cb7c2316e59eee7e203102a7775}
Chatbot
In this challenge, I used IDA to disassemble the program and then I see some useful information

which means this is a pyinstaller generated executable file. Knowing that, I used pyinstxtractor.py to extract the file.
While inspecting file main.pyc, since I cannot install decompyle3 (skill issues), I used a decompiler online and got main.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
| import base64
import json
import time
import random
import sys
import os
from ctypes import CDLL, c_char_p, c_int, c_void_p
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding
import 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 = None
check_integrity = None
decrypt_flag_file = None
free_mem = None
try:
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 = None
except 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 ok:
print('[!] Integrity failed or debugger detected. Exiting.')
sys.exit(1)
PUB_PEM = b'-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsJftFGJC6RjAC54aMncA\nfjb2xXeRECiwHuz2wC6QynDd93/7XIrqTObeTpfBCSpOKRLhks6/nzZFTTshttps://hackmd.io/uC9DCV3lSUqkaUnt-NlQzAYdQCj\n4roXhWo5lFfH0OTL+164VoKnmUkQ9dppzpmV0Kpk5IQhEyuPYzJfFAlafcHdQvUo\nidkqcOPpR7hznJPEuRbPxJod34Bph/u9vePKcQQfe+/l/nn02nbfYWTuGtuEdpHq\nMkktl4WpB50/a5ZqYkW4z0zjFCY5LIPE7mpUNLrZnadBGIaLoVV2lZEBdLt6iLkV\nHXIr+xNA9ysE304T0JJ/DwM1OXb4yVrtawbFLBu9otOC+Gu0Set+8OjfQvJ+tlT/\nzQIDAQAB\n-----END PUBLIC KEY-----'
public_key = None
try:
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')
except Exception as e:
return (False, str(e))
else: # inserted
return (True, j)
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 ok:
if decrypt_flag_file is None:
print('Native library not available -> cannot decrypt')
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()
|
1
2
3
4
5
6
7
8
9
| def load_native_lib(name):
return CDLL(get_resource_path(name))
if sys.platform == 'win32':
LIBNAME = 'libnative.dll'
else: # inserted
LIBNAME = 'libnative.so'
...
try:
lib = load_native_lib(LIBNAME)
|
Looking at the flag-decrypting part after verifying token, There’s a function decrypt_flag_file which decrypt the encoded flag file from its path. Also, return to the top of this code, this function is imported from libnative.so (or libnative.dll) and here i got libnative.so so I used IDA to inspect the file to see what it do to decrypt the flag

decrypt_flag_file function calls recover_key

recover_key just deobfuscate the OBF_KEY with MASK through bunch of XOR operation to get the original key

back to decrypt_flag_file, this program reads the first 16 bytes from flag.enc file as iv and the rest as ciphertext. It also compare the length of key with 0x1F in order to decide which decryption to use for each case
so that’s all of the decryption logic
this is my script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| #!/usr/bin/env python3
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os
OBF_KEY = [
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
]
MASK = [0x2a, 0x2a, 0xa, 0x9a]
def recover_key():
recovered_key = bytearray(32)
recovered_key[0] = 0xC4
for i in range(1, 32):
mask_byte = MASK[i & 3]
recovered_key[i] = OBF_KEY[i] ^ mask_byte
return bytes(recovered_key)
key = recover_key()
def decrypt_flag_file(filename):
key_len = len(key)
with open(filename, "rb") as f:
iv = f.read(16)
ct = f.read()
cipher_alg = algorithms.AES256(key)
cipher = Cipher(cipher_alg, modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
unpadded_plaintext = decryptor.update(ct) + decryptor.finalize()
return unpadded_plaintext
def main():
ENCRYPTED_FILE_NAME = "flag.enc"
decrypted_data = decrypt_flag_file(ENCRYPTED_FILE_NAME)
if decrypted_data:
print(decrypted_data.decode('utf-8'))
if __name__ == "__main__":
main()
|
CSCV2025{reversed_vip*_chatbot_bypassed}
Comments