CAT Reloaded CTF 2025 - Pickle Challenge walkthrough
دا حلي ل Pickle CTF يوم ما اشتغلنا انا والتيم

بعد شوية معاناة في تسطيب python 3.9 , فكيت الrar file وكان فيه تلت فايلات
run.py
rehyd.py
chall.pkl
خلينا نشوف run.py الاول

هنا هو بيعمل import لrehyd و بيفتح chall.pkl فالread byte mode
لما تشوف محتويات chall.pkl هتلاقيها مشوهة او obfuscated

طيب خلينا نشوف rehyd.py

هنا بيفك ضغط كود مموه وبيعمل execution طب خلينا نجرب نشغل run.py ونشوف هيحصل ايه

يدوبك بس بيشوف جبت الحل ولا لا
طبعا دا الى حد ما اسهل من تحليل exe او اي compiled code
نرجع لrehyd.py انا عايز دلوقت اشوف الكود بعد ما بيتفك الضغط و الdeobfuscation بتاع ايا كان اللي بيعمله.
اسهل حاجة اني اعمله dump بالfile operations بتاعت python و اشوف الكود بس هما عقدوها حنة زيادة.
مرحلة فك الضغط و نقلها للمتغير code بيخلي نوع الobject دا من class code
من خصائص الcode object/class في python انه بيحتوي عالcompiled bytecode و ايوه في حاجة اسمها compiled python وهي عملية تحويل الpython source code لbytecode بتبقى عبارة عن instructions بتنفذها الPVM (python vm) والextenstion بتاعها .pyc
بعدها اتخضيت عشان اول مرة اسمع عن debugging python bytecode بس حلها كان سهل عشان اول ما يبقى عندي الbytecode نفسه اقدر في ثانية احوله لpython source code عادي مش زي صعوبة C/C++
قولت خلاص اشوف حل اعرف منه ا dump الdisassembled bytecode لفايل لوحده عشان اكمل تحاليل عليه.
فا استعنت بالAI انه يتولى موضوع حساب الmagic bytes والheaders بتاعت الpyc ويعملها dump فالاخر

كدا ممكن اشغل run.py تاني

عملت print لللdisassembled bytecode و سيفت الbytecode في فايل. خلينا نشوف محتوى الفايل دا ايه

طبعا عبارة عن unreadable data بس بقينا اقرب للحل, دلوقت عايزين نحوله لreadable source code
هخش ادور على اي python bytecode decompiler اونلاين وادخل الفايل وانزل الsource code

لما فتحت الdecompiled file اللي هي المفروض اخر مرحلة خلاص اتفاجأت بالمنظر دا

حوالي 3500 سطر من الfunctions والcalculations ساعتها بقى انا فصلت بتاع خمس ساعات ورجعتلها تاني
الفايل عبارة عن functions بس و كل واحدة فيهم بتعمل الtests بتاعتها فا اكيد انا مش هشتغل bruteforce واحسب كل function لوحدها
فكرت هو فين اصلا الentry point بتاعت الفايل يعني انهي function بيتعملها call
روحت رجعت على rehyd.py وهنا لقيت انها بترجع حاجة اسمها ns[entry]

قولت بس خلاص اعملها print كمان و اشغل run.py اشوف ايه دي


بس خلاص كدا عرفت اني Chimpanzini_Bananini هي الحل للموضوع كله
كدا نبتدي نحلل الfunction دي على رواقة ونطلع الflag واحدة واحدة
دي الfunction snippet
def Chimpanzini_Bananini():
password = input('Enter the Flag: ')
part1 = xor(password[0:5].encode(), password[5:10].encode())
if part1.hex() != '302e0b1933':
print('wrong')
return
part2 = zlib.crc32(password[10:14].encode())
if part2 != 3979310991:
print('wrong')
return
part3 = zlib.crc32(password[14:18].encode())
if part3 != 448183154:
print('wrong')
return
part4 = xor(password[0:18].encode(), password[18:36].encode())
if part4.hex() != '70373134241b5c6b2d2c6b42076f2c442a2b':
print('wrong')
return
part5 = hashlib.md5(password[36:38].encode()).hexdigest()
if part5 != '346b81a32e7007eccadf60252bb599f0':
print('wrong')
return
part6 = hashlib.md5(password[38:40].encode()).hexdigest()
if part6 != '2c3ba657da75eab82c88c429fbbf2207':
print('wrong')
return
part7 = tralalero_tralala('flag{real_is_rare__fake_is_everywhere}', password[40:58])
if part7 != '3856abb119718a174973a5fbbf46727f419c':
print('wrong')
return
print('Flag is correct!')
نشوف part1:
واضح انه بيعمل xor لجزء من الinput بتاعك مع جزء تاني من الinput بتاعك و يقارن النتيجة بالhex value دي 302e0b1933
هنا مش هقدر اعمل اي reverse engineering عشان مش عارف ولا جزء من المعادلة غير النتيجة وانا محتاج عالاقل جزئين من المعادلة.
عملت skip ودخلت على part2:
هنا اسهل شوية, بياخد جزء من الinput وبيعمله crc32 checksum وبيقارن النتيجة لhardcoded value
بس خلاص كدا هنكتب سكريبت يعكس الcrc32 checksum وياخد الhardcoded value ويرجعلنا جزء من الflag
هنا استخدمت الAI في انه يكتبلي سكريبت يعكس الalgorithm ودي كانت النتيجة بعد ما عدلت:
import zlib
import itertools
import string
target_crc = 3979310991
# You can customize charset here, e.g.:
charset = string.ascii_letters + string.digits + string.punctuation + ' '
def brute_force_crc32(target_crc, length=4):
print(f"Starting brute force for CRC32={target_crc} over {length} bytes...")
count = 0
for candidate_tuple in itertools.product(charset, repeat=length):
candidate = ''.join(candidate_tuple)
crc = zlib.crc32(candidate.encode())
count += 1
if crc == target_crc:
print(f"Found match: '{candidate}'")
# Return here if you want only first result, or continue to find all
return candidate
if count % 1000000 == 0:
print(f"Checked {count} candidates so far...")
print("No matches found.")
return None
result = brute_force_crc32(target_crc)
print("Result:", result)

كدا طلعنا password[10:14] و هتساوي _4ve
نخش على part3 :
نفس الكلام بس value مختلفة فا طبقت نفس السكريبت بس غيرت الtarget_crc ل 448183154

كدا طلعنا password[14:18] و هيساوي _Y0u
نخش على part4:
هنعمله skip دلوقت عشان نفس فكرة part1
نخش على part5:
هنا بيعمل hash لجزء من الpassword بالmd5 وبيحوله لhex ويقارنه بhardcoded hex value والmd5 طبعا ضعيف جدا
استخدمت الAI عشان يcrack الmd5 hash و طلعت بالسكريبت دا بعد التعديل:
import hashlib
import string
import itertools
target_hash = '346b81a32e7007eccadf60252bb599f0'
charset = string.printable.strip() # printable ASCII, without whitespace
def brute_force_md5(target_hash):
for c1, c2 in itertools.product(charset, repeat=2):
candidate = c1 + c2
if hashlib.md5(candidate.encode()).hexdigest() == target_hash:
print(f"Found match: '{candidate}'")
return candidate
print("No match found.")
return None
result = brute_force_md5(target_hash)
print("Result:", result)

كدا طلعنا password[36:38] و هيساوي h1
نخش على part6:
نفس فكرة الmd5 hashing فا استخدمت نفس السكريبت بس غيرت الtarget hash

كدا طلعنا password[38:40] وهيساوي _s
نخش على part7:
هنا اختلفت شوية, هو بcall function اسمها tralalero_tralala باتنين args واحد فيهم hardcoded والتاني جزء من الinput وبيقارن الreturn value بvalue تانية
هنا دخلت اشوف الfunction دي بتعمل ايه
def tralalero_tralala(key, plain):
S = list(range(256))
j = 0
key_bytes = key.encode()
for i in range(256):
j = (j + S[i] + key_bytes[i % len(key_bytes)]) % 256
S[i], S[j] = (S[j], S[i])
i = j = 0
result = []
for char in plain.encode():
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = (S[j], S[i])
K = S[(S[i] + S[j]) % 256]
result.append(char ^ K)
return ''.join((f'{b:02x}' for b in result))
دي encryption algorithm بسيطة جدا واستخدمت الAI في انه اعرف ايه هي و طلعت RC4 و RC4 دي symmetric encryption algorithm فا مفتاح التشفير هو هو مفتاح فك التفشير فا عشان تجيب الplaintext هتروح لأي online rc4 decryption tool input و هتحط الencyrpted value دي 3856abb119718a174973a5fbbf46727f419c و تحط custom decryption key اللي هي دي flag{real_is_rare__fake_is_everywhere}

دلوقت احنا عندنا الاجزاء دي من الflag
password[0:5] = CATF{ default format
password[5:10] = UNKNOWN
password[10:14] = 4ve_
password[14:18] = Y0u_
password[18:36] = UNKNOWN
password[36:38] = h1
password[38:40] = s_
password[40:58] = cucumb3red_th1ng?}
كدا فاضل جزئين ناقصين فالflag فا نرجع على part 1:
كدا احنا عندنا جزئين من معادلة الXOR اللي هما النتيجة و password[0:5]
استعنت بالAI وكتبت سكريبت عشان يطلع الvalue الناقصة ودا السكريبت:
import string
def xor_bytes(b1, b2):
return bytes([x ^ y for x, y in zip(b1, b2)])
# Fixed input A
A = b'CATF{'
# XOR target
target_xor = bytes.fromhex('302e0b1933')
# Compute B directly: B = A XOR target
B = xor_bytes(A, target_xor)
print(f"len(A) => {len(A)}")
print(f"len(B) => {len(B)}")
# Check if B is printable
if all(32 <= b < 127 for b in B):
print(f"A = {A.decode()}")
print(f"B = {B.decode()}")
print(f"A XOR B = {target_xor.hex()}")
else:
print(f"B contains non-printable characters: {B.decode(errors='replace')}")

كدا طلعنا password[5:10] و بيساوي so__H
هنعمل نفس الكلام في part4:
بس هنغير المتغير A ل CATF{so__H4ve_Y0u_
وهنغير الtarget xor للhex اللي هيتقارن بيه part4
و هيطلع باقي الflag اللي هو password[18:36] هيساوي
3ver_h34rd_4bout_t
فالflag على بعض هيبقى
CATF{so__H4ve_Y0u_3ver_h34rd_4bout_th1s_cucumb3red_th1ng?}


