## 1. Evil ECB (18 Solves) ### Description: - You're used to CBC authentication, but how about ECB - Challenge file: ```python= from Crypto.Cipher import AES from Crypto.Util.Padding import pad, unpad from os import urandom import json import socket import threading flag = 'KCSC{s0m3_r3ad4ble_5tr1ng_like_7his}' menu = ('\n\n|---------------------------------------|\n' + '| Welcome to Evil_ECB! |\n' + '| Maybe we can change the Crypto world |\n' + '| with a physical phenomena :D |\n' + '|---------------------------------------|\n' + '| [1] Login |\n' + '| [2] Register ^__^ |\n' + '| [3] Quit X__X |\n' + '|---------------------------------------|\n') bye = ( '[+] Closing Connection ..\n'+ '[+] Bye ..\n') class Evil_ECB: def __init__(self): self.key = urandom(16) self.cipher = AES.new(self.key, AES.MODE_ECB) self.users = ['admin'] def login(self, token): try: data = json.loads(unpad(self.cipher.decrypt(bytes.fromhex(token)), 16).decode()) if data['username'] not in self.users: return '[-] Unknown user' if data['username'] == "admin" and data["isAdmin"]: return '[+] Hello admin , here is your secret : %s\n' % flag return "[+] Hello %s , you don't have any secret in our database" % data['username'] except: return '[-] Invalid token !' def register(self, user): if user in self.users: return '[-] User already exists' data = b'{"username": "%s", "isAdmin": false}' % (user.encode()) token = self.cipher.encrypt(pad(data, 16)).hex() self.users.append(user) return '[+] You can use this token to access your account : %s' % token class ThreadedServer(object): def __init__(self, host, port): self.host = host self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.host, self.port)) def listen(self): self.sock.listen(5) while True: client, address = self.sock.accept() client.settimeout(60) threading.Thread(target = self.listenToClient,args = (client,address)).start() def listenToClient(self, client, address): size = 1024 chal = Evil_ECB() client.send(menu.encode()) for i in range(10): try: client.send(b'> ') choice = client.recv(size).strip() if choice == b'1': client.send(b'Token: ') token = client.recv(size).strip().decode() client.send(chal.login(token).encode() + b'\n') elif choice == b'2': client.send(b'Username: ') user = client.recv(size).strip().decode() client.send(chal.register(user).encode() + b'\n') elif choice == b'3': client.send(bye.encode()) client.close() else: client.send(b'Invalid choice!!!!\n') client.close() except: client.close() return False client.send(b'No more rounds\n') client.close() if __name__ == "__main__": ThreadedServer('',2003).listen() ``` ### Solution - Đây có lẽ là bài đơn giản nhất trong các bài mà mình gặp trong KCSC CTF 2024 vì cách attack khá clear bằng cách `Cut and Paste ECB`, mọi người có thể tham khảo các ví dụ ở [đây](https://bernardoamc.com/ecb-cut-paste-attack/#:~:text=Let%E2%80%99s%20recap%20what%20we%20want%20to%20achieve%3A%201,Send%203%20more%20characters%20so%20we%20have%20%3Cour_3_characters%3E%26uid%3D10%26role%3D), mình sẽ chỉ viết cách tấn công sơ lược mà thôi. - Về flow của bài, server sẽ cung cấp cho chúng ta hai chức năng chính là `Login` và `Register`. `Register` dùng để thêm `user` vào database của server và trả lại cho người dùng token để `Login`. `Login` sẽ xác thực chúng ta có thỏa điều kiện `data['username'] == "admin" and data["isAdmin"]` không để nhả ra `flag`. - Ở đây mình sẽ phải giải quyết hai vấn đề chính, đó là chọn `username` như thế nào, và `pad` ra sao cho đúng với thuật toán PKCS7. Theo như link bài viết ở trên, mình sẽ cố gắng xử lí hai phần trên thành 2 block riêng rồi nối lại với nhau. Về cơ bản phần này chỉ cần test đi test lại nhiều lần là xong thôi, mình chọn `username` và `pad` như sau: ```= user = 'XX{"username": "admin", "isAdmin": true}' add = (b'XX' + b'\x10' * 16).decode() ``` - Khi đang trong lab ngồi làm thì mình đã test khá nhiều, có cả một file riêng ngồi thêm thêm bớt bớt các thứ với ông **Minh_AT20** lỏ: ```python= import json from Crypto.Util.Padding import pad, unpad from Crypto.Cipher import AES key = b'\x00' * 16 cipher = AES.new(key=key, mode=AES.MODE_ECB) def reg(user): data = b'{"username": "%s", "isAdmin": false}' % (user.encode()) token = cipher.encrypt(pad(data, 16)).hex() return token def login(token): # data = json.loads(unpad(cipher.decrypt(bytes.fromhex(token)), 16).decode()) temp = cipher.decrypt(bytes.fromhex(token)).decode() return temp user = 'XX{"username": "admin", "isAdmin": true}' token = reg(user) data = login(token) token = bytes.fromhex(token)[16:48+16] check_user = login(bytes.hex(token)) add = (b'XX' + b'\x10' * 16).decode() token_add = reg(add) data_add = login(token_add) token_add = bytes.fromhex(token_add)[16:32] check_user = login(bytes.hex(token_add)) send = token + token_add verify = json.loads(unpad(cipher.decrypt(send), 16)) if verify['username'] == "admin" and verify["isAdmin"]: print(verify) print('You won') ``` Trên đây là cách mình test - Còn đây là script giải của mình: ```python= '''nc 103.163.24.78 2003''' from pwn import * from json import loads HOST = '103.163.24.78' PORT = 2003 r = remote(HOST, PORT) print(r.recv().decode()) user = 'XX{"username": "admin", "isAdmin": true}' add = (b'XX' + b'\x10' * 16).decode() r.sendlineafter(b'> ', b'2') r.sendlineafter(b'Username: ', user.encode()) r.recvuntil(b' : ') token = bytes.fromhex(r.recvline().decode())[16:48+16] r.sendlineafter(b'> ', b'2') r.sendlineafter(b'Username: ', add.encode()) r.recvuntil(b' : ') token_add = bytes.fromhex(r.recvline().decode())[16:32] send = token + token_add r.sendlineafter(b'> ', b'1') r.sendlineafter(b'Token: ', bytes.hex(send).encode()) flag = r.recvline().decode() print(flag) ``` ### Flag > ~~`KCSC{eCb_m0de_1s_4lways_1nSecUre_:))}`~~ ### Note - Mình viết wu bài này để phù hợp với các bạn đã có base kha khá ở `crypto` và hiểu cách padding, nếu chưa thì [đây](https://node-security.com/posts/cryptography-pkcs-7-padding/) là cách mà `ECB` đã dùng. --- ## 2. Miscrypt (12 Solves) ### Description - just misc warm up - [Challenge file](https://kcsc.tf/files/ced8fa249230852a878c2e430713a63e/Miscrypt.zip?token=eyJ1c2VyX2lkIjozMywidGVhbV9pZCI6MTIsImZpbGVfaWQiOjI2fQ.ZkQZlA.IZhARosGmmU84JIj-nxokIutVKE) (Trong trường hợp link dead thì mình sẽ update lại sau) ### Solution - Đây là bài mình nghĩ hướng giải cũng rất clear và dễ, nhưng không hiểu sao lúc làm bài mình lại không thể bình tĩnh lại mà solve. - Về cơ bản thì flow của bài sẽ là: ``` flag -> QR -> Encrypted QR (EQR) ``` - `QR` sẽ được gen trong file `gen_qr_flag` còn `EQR` thì là file `chall.py`. Phần quan trọng nằm ở trong file `chall.py` nên mình sẽ nói sâu về nó hơn. - Trước hết, từ `QR` chall sẽ gen ra các dữ liệu của ảnh, sau đó tạo ra một ma trận `M` cỡ 3x3 trong trường `Galois256`. Author đã cho scan từ cột sang hàng, mỗi 3 phần tử liền nhau trên một cột sẽ được lưu vào ma trận `A`. Sau đó thực hiện: ``` M = A + M out = M ``` và cứ thế tiếp tục cho đến khi scan hết 999x999 pixels - Mình cần recover lại giá trị `A` ban đầu, vậy thì cần tìm ma trận `M`, nhưng cũng rất may là khi nhìn vào `QR`, chúng ta biết được nó chỉ có hai màu đen trắng, và các pixels từ đoạn `[0, 2]` và `[3, 5]` cột 1 đều là màu trắng, vậy trong `EQR` thì chúng sẽ hơn kém nhau đúng một ma trận `M` (viết vậy cho dễ hiểu), sau khi test thì mình nhận ra ma trận `M` không ở đâu xa, nó chính là đoạn `[3, 5]` đó luôn (mình đã test bằng cách tự tạo ra flag và giá trị của `M`, sau đó đem đi cộng trừ blabla để tìm lại), cụ thể có thể recover như sau: ```python= for y in range(3, 6, 3): M = GF256([pixels[1, y], pixels[1, y+1], pixels[1, y+2]], dtype=np.uint8) ``` - Tiếp theo là đến đoạn recover lại giá trị của `A`, chúng ta sẽ phải dùng `np.subtract` và sau đó update giá trị của `M` để giải mã tiếp: ```python= out = GF256([pixels[x, y], pixels[x, y+1], pixels[x, y+2]]) A = np.subtract(out, M) M = np.add(A, M) pixels[x, y], pixels[x, y+1], pixels[x, y+2] = [tuple([int(i) for i in j]) for j in A] ``` - Script của mình: ```python= from PIL import Image from tqdm import tqdm import numpy as np import galois GF256 = galois.GF(2**8) img = Image.open('qr_flag_encrypt.png') pixels = img.load() width, height = img.size for y in range(3, 6, 3): M = GF256([pixels[1, y], pixels[1, y+1], pixels[1, y+2]], dtype=np.uint8) for x in tqdm(range(width)): for y in range(0,height,3): out = GF256([pixels[x, y], pixels[x, y+1], pixels[x, y+2]]) A = np.subtract(out, M) M = np.add(A, M) pixels[x, y], pixels[x, y+1], pixels[x, y+2] = [tuple([int(i) for i in j]) for j in A] img.show() ``` - Đây là ảnh sau khi show: ![image](https://hackmd.io/_uploads/ryskxjWXA.png) ### Flag > ~~`KCSC{CrYpt0-l1k3-St3g4n0???}`~~ --- ## 3. KCSC Square (10 Solves) ### Description - Come to our square and gain glory! - Challenge file: ```python= class AES: sbox = ( 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16, ) rcon = (0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36) gmul2 = ( 0x00, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c, 0x1e, 0x20, 0x22, 0x24, 0x26, 0x28, 0x2a, 0x2c, 0x2e, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3a, 0x3c, 0x3e, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4a, 0x4c, 0x4e, 0x50, 0x52, 0x54, 0x56, 0x58, 0x5a, 0x5c, 0x5e, 0x60, 0x62, 0x64, 0x66, 0x68, 0x6a, 0x6c, 0x6e, 0x70, 0x72, 0x74, 0x76, 0x78, 0x7a, 0x7c, 0x7e, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8a, 0x8c, 0x8e, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9a, 0x9c, 0x9e, 0xa0, 0xa2, 0xa4, 0xa6, 0xa8, 0xaa, 0xac, 0xae, 0xb0, 0xb2, 0xb4, 0xb6, 0xb8, 0xba, 0xbc, 0xbe, 0xc0, 0xc2, 0xc4, 0xc6, 0xc8, 0xca, 0xcc, 0xce, 0xd0, 0xd2, 0xd4, 0xd6, 0xd8, 0xda, 0xdc, 0xde, 0xe0, 0xe2, 0xe4, 0xe6, 0xe8, 0xea, 0xec, 0xee, 0xf0, 0xf2, 0xf4, 0xf6, 0xf8, 0xfa, 0xfc, 0xfe, 0x1b, 0x19, 0x1f, 0x1d, 0x13, 0x11, 0x17, 0x15, 0x0b, 0x09, 0x0f, 0x0d, 0x03, 0x01, 0x07, 0x05, 0x3b, 0x39, 0x3f, 0x3d, 0x33, 0x31, 0x37, 0x35, 0x2b, 0x29, 0x2f, 0x2d, 0x23, 0x21, 0x27, 0x25, 0x5b, 0x59, 0x5f, 0x5d, 0x53, 0x51, 0x57, 0x55, 0x4b, 0x49, 0x4f, 0x4d, 0x43, 0x41, 0x47, 0x45, 0x7b, 0x79, 0x7f, 0x7d, 0x73, 0x71, 0x77, 0x75, 0x6b, 0x69, 0x6f, 0x6d, 0x63, 0x61, 0x67, 0x65, 0x9b, 0x99, 0x9f, 0x9d, 0x93, 0x91, 0x97, 0x95, 0x8b, 0x89, 0x8f, 0x8d, 0x83, 0x81, 0x87, 0x85, 0xbb, 0xb9, 0xbf, 0xbd, 0xb3, 0xb1, 0xb7, 0xb5, 0xab, 0xa9, 0xaf, 0xad, 0xa3, 0xa1, 0xa7, 0xa5, 0xdb, 0xd9, 0xdf, 0xdd, 0xd3, 0xd1, 0xd7, 0xd5, 0xcb, 0xc9, 0xcf, 0xcd, 0xc3, 0xc1, 0xc7, 0xc5, 0xfb, 0xf9, 0xff, 0xfd, 0xf3, 0xf1, 0xf7, 0xf5, 0xeb, 0xe9, 0xef, 0xed, 0xe3, 0xe1, 0xe7, 0xe5 ) gmul3 = ( 0x00, 0x03, 0x06, 0x05, 0x0c, 0x0f, 0x0a, 0x09, 0x18, 0x1b, 0x1e, 0x1d, 0x14, 0x17, 0x12, 0x11, 0x30, 0x33, 0x36, 0x35, 0x3c, 0x3f, 0x3a, 0x39, 0x28, 0x2b, 0x2e, 0x2d, 0x24, 0x27, 0x22, 0x21, 0x60, 0x63, 0x66, 0x65, 0x6c, 0x6f, 0x6a, 0x69, 0x78, 0x7b, 0x7e, 0x7d, 0x74, 0x77, 0x72, 0x71, 0x50, 0x53, 0x56, 0x55, 0x5c, 0x5f, 0x5a, 0x59, 0x48, 0x4b, 0x4e, 0x4d, 0x44, 0x47, 0x42, 0x41, 0xc0, 0xc3, 0xc6, 0xc5, 0xcc, 0xcf, 0xca, 0xc9, 0xd8, 0xdb, 0xde, 0xdd, 0xd4, 0xd7, 0xd2, 0xd1, 0xf0, 0xf3, 0xf6, 0xf5, 0xfc, 0xff, 0xfa, 0xf9, 0xe8, 0xeb, 0xee, 0xed, 0xe4, 0xe7, 0xe2, 0xe1, 0xa0, 0xa3, 0xa6, 0xa5, 0xac, 0xaf, 0xaa, 0xa9, 0xb8, 0xbb, 0xbe, 0xbd, 0xb4, 0xb7, 0xb2, 0xb1, 0x90, 0x93, 0x96, 0x95, 0x9c, 0x9f, 0x9a, 0x99, 0x88, 0x8b, 0x8e, 0x8d, 0x84, 0x87, 0x82, 0x81, 0x9b, 0x98, 0x9d, 0x9e, 0x97, 0x94, 0x91, 0x92, 0x83, 0x80, 0x85, 0x86, 0x8f, 0x8c, 0x89, 0x8a, 0xab, 0xa8, 0xad, 0xae, 0xa7, 0xa4, 0xa1, 0xa2, 0xb3, 0xb0, 0xb5, 0xb6, 0xbf, 0xbc, 0xb9, 0xba, 0xfb, 0xf8, 0xfd, 0xfe, 0xf7, 0xf4, 0xf1, 0xf2, 0xe3, 0xe0, 0xe5, 0xe6, 0xef, 0xec, 0xe9, 0xea, 0xcb, 0xc8, 0xcd, 0xce, 0xc7, 0xc4, 0xc1, 0xc2, 0xd3, 0xd0, 0xd5, 0xd6, 0xdf, 0xdc, 0xd9, 0xda, 0x5b, 0x58, 0x5d, 0x5e, 0x57, 0x54, 0x51, 0x52, 0x43, 0x40, 0x45, 0x46, 0x4f, 0x4c, 0x49, 0x4a, 0x6b, 0x68, 0x6d, 0x6e, 0x67, 0x64, 0x61, 0x62, 0x73, 0x70, 0x75, 0x76, 0x7f, 0x7c, 0x79, 0x7a, 0x3b, 0x38, 0x3d, 0x3e, 0x37, 0x34, 0x31, 0x32, 0x23, 0x20, 0x25, 0x26, 0x2f, 0x2c, 0x29, 0x2a, 0x0b, 0x08, 0x0d, 0x0e, 0x07, 0x04, 0x01, 0x02, 0x13, 0x10, 0x15, 0x16, 0x1f, 0x1c, 0x19, 0x1a ) def __init__(self, key): self._block_size = 16 self._round_keys = self._expand_key([i for i in key]) self._state = [] def _transpose(self, m): return [m[4 * j + i] for i in range(4) for j in range(4)] def _xor(self, a, b): return [x ^ y for x, y in zip(a, b)] def _expand_key(self, key): round_keys = [key] for i in range(4): round_key = [] first = round_keys[i][:4] last = round_keys[i][-4:] last = last[1:] + [last[0]] last = [self.sbox[i] for i in last] round_key.extend(self._xor(self._xor(first, last), [self.rcon[i+1], 0, 0, 0])) for j in range(0, 12, 4): round_key.extend(self._xor(round_key[j:j + 4], round_keys[i][j + 4:j + 8])) round_keys.append(round_key) for i in range(len(round_keys)): round_keys[i] = self._transpose(round_keys[i]) return round_keys def _add_round_key(self, i): self._state = self._xor(self._round_keys[i], self._state) def _mix_columns(self): s = [0] * self._block_size for i in range(4): s[i] = self.gmul2[self._state[i]] ^ self.gmul3[self._state[i + 4]] ^ self._state[i + 8] ^ self._state[i + 12] s[i + 4] = self._state[i] ^ self.gmul2[self._state[i + 4]] ^ self.gmul3[self._state[i + 8]] ^ self._state[i + 12] s[i + 8] = self._state[i] ^ self._state[i + 4] ^ self.gmul2[self._state[i + 8]] ^ self.gmul3[self._state[i + 12]] s[i + 12] = self.gmul3[self._state[i]] ^ self._state[i + 4] ^ self._state[i + 8] ^ self.gmul2[self._state[i + 12]] self._state = s def _shift_rows(self): self._state = [ self._state[0], self._state[1], self._state[2], self._state[3], self._state[5], self._state[6], self._state[7], self._state[4], self._state[10], self._state[11], self._state[8], self._state[9], self._state[15], self._state[12], self._state[13], self._state[14] ] def _sub_bytes(self): self._state = [self.sbox[i] for i in self._state] def _encrypt_block(self): self._add_round_key(0) for i in range(1, 4): self._sub_bytes() self._shift_rows() self._mix_columns() self._add_round_key(i) self._sub_bytes() self._shift_rows() self._add_round_key(4) def encrypt(self, plaintext): ciphertext = b'' self._state = self._transpose([c for c in plaintext]) self._encrypt_block() ciphertext += bytes(self._transpose(self._state)) from os import urandom from aes import AES import socket import threading flag = 'KCSC{s0m3_r3ad4ble_5tr1ng_like_7his}' menu = ('\n\n|---------------------------------------|\n' + '| Welcome to KCSC Square! |\n' + '| I know it\'s late now but |\n' + '| Happy Reunification Day :D |\n' + '|---------------------------------------|\n' + '| [1] Get ciphertext |\n' + '| [2] Guess key ^__^ |\n' + '| [3] Quit X__X |\n' + '|---------------------------------------|\n') bye = ( '[+] Closing Connection ..\n'+ '[+] Bye ..\n') class ThreadedServer(object): def __init__(self, host, port): self.host = host self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.host, self.port)) def listen(self): self.sock.listen(5) while True: client, address = self.sock.accept() client.settimeout(60) threading.Thread(target = self.listenToClient,args = (client,address)).start() def listenToClient(self, client, address): size = 1024 key = urandom(16) chal = AES(key) client.send(menu.encode()) for i in range(8888): try: client.send(b'> ') choice = client.recv(size).strip() if choice == b'1': client.send(b'Plaintext in hex: ') hex_pt = client.recv(size).strip().decode() try: pt = bytes.fromhex(hex_pt) assert len(pt) == 16 except: client.send(b'Something wrong in your plaintext' + b'\n') continue client.send(chal.encrypt(pt).hex().encode() + b'\n') elif choice == b'2': client.send(b'Key in hex: ') hex_key = client.recv(size).strip().decode() try: guess_key = bytes.fromhex(hex_key) assert guess_key == key except: client.send(b'Wrong key, good luck next time =)))' + b'\n') client.close() client.send(b'Nice try, you got it :D!!!! Here is your flag: ' + flag.encode() + b'\n') client.close() elif choice == b'3': client.send(bye.encode()) client.close() else: client.send(b'Invalid choice!!!!\n') client.close() except: client.close() return False client.send(b'No more rounds\n') client.close() if __name__ == "__main__": ThreadedServer('',2004).listen() return ciphertext ``` - Challenge server: ```python= from os import urandom from aes import AES import socket import threading flag = 'KCSC{s0m3_r3ad4ble_5tr1ng_like_7his}' menu = ('\n\n|---------------------------------------|\n' + '| Welcome to KCSC Square! |\n' + '| I know it\'s late now but |\n' + '| Happy Reunification Day :D |\n' + '|---------------------------------------|\n' + '| [1] Get ciphertext |\n' + '| [2] Guess key ^__^ |\n' + '| [3] Quit X__X |\n' + '|---------------------------------------|\n') bye = ( '[+] Closing Connection ..\n'+ '[+] Bye ..\n') class ThreadedServer(object): def __init__(self, host, port): self.host = host self.port = port self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.sock.bind((self.host, self.port)) def listen(self): self.sock.listen(5) while True: client, address = self.sock.accept() client.settimeout(60) threading.Thread(target = self.listenToClient,args = (client,address)).start() def listenToClient(self, client, address): size = 1024 key = urandom(16) chal = AES(key) client.send(menu.encode()) for i in range(8888): try: client.send(b'> ') choice = client.recv(size).strip() if choice == b'1': client.send(b'Plaintext in hex: ') hex_pt = client.recv(size).strip().decode() try: pt = bytes.fromhex(hex_pt) assert len(pt) == 16 except: client.send(b'Something wrong in your plaintext' + b'\n') continue client.send(chal.encrypt(pt).hex().encode() + b'\n') elif choice == b'2': client.send(b'Key in hex: ') hex_key = client.recv(size).strip().decode() try: guess_key = bytes.fromhex(hex_key) assert guess_key == key except: client.send(b'Wrong key, good luck next time =)))' + b'\n') client.close() client.send(b'Nice try, you got it :D!!!! Here is your flag: ' + flag.encode() + b'\n') client.close() elif choice == b'3': client.send(bye.encode()) client.close() else: client.send(b'Invalid choice!!!!\n') client.close() except: client.close() return False client.send(b'No more rounds\n') client.close() if __name__ == "__main__": ThreadedServer('',2004).listen() ``` ### Solution - Về cơ bản, đây là một bài AES với các cách cài đặt giống hệt với loại thường gặp trong crypto, tuy nhiên số vòng đã bị giảm đi thay vì 10 vòng còn 4 vòng (trong đó 3 vòng vẫn giữ nguyên 4 bước add, sub, shift, mix và vòng cuối không còn mix). Điều này đã trực tiếp làm giảm đi tính bảo mật hiệu quả của AES 10 rounds, bởi nó leak ra `key` của bài. Ý tưởng của bài là dựa trên tính chất này, nói cách khác, đây là loại `Square Attack`. Về cách tấn công mình tham khảo ở [đây](https://www.davidwong.fr/blockbreakers/square.html) và thấy cũng khá đầy đủ (thực ra phải tham khảo thêm code của anh **Giáp-UIT** và **nhatzittt** nữa :monkey:) - `Square Attack` cũng có thể được coi là một loại `chosen-plaintext attack`, chúng ta có thể nhập `pt` vào và nhận lại `ct`, sau đó từ mối liên hệ có thể tìm ra được `key`. Cụ thể như thế nào thì link trên đã giải đáp toàn bộ, mình chỉ khái lược lại các bước làm thôi. #### Idea **Input**: `plaintexts` **Output**: `key` - Trước hết cần biết rằng cách attack này hoàn toàn dựa trên cấu trúc của AES Square, nói cách khác chúng ta cần hiểu rõ AES mã hóa qua các bước nào và lỗ hổng ở đâu. - Với `key` có 16 bytes mà chúng ta cần tìm lại thì thuật toán đều giống nhau hết, mình sẽ làm ví dụ ở đúng một byte đầu, và các byte sau làm hoàn toàn tương tự. ##### Thuật toán tìm `Roundkey` cuối: - Chúng ta sẽ bắt đầu bằng việc tạo ra 256 `pt` khác nhau, 256 chính là không gian mẫu các trường hợp có thể xảy ra của `index` 1 byte, và bằng sơ đồ này mình nghĩ có thể nói lên toàn bộ quá trình mã hóa của các `pt` đó: ![image](https://hackmd.io/_uploads/rJUMk6fXR.png) - Byte màu xanh nhỏ nhỏ ở trên cùng góc trái chính là `index` của chúng ta, các byte màu trắng tạm thời bỏ qua. Chạy dọc theo thuật toán mã hóa của AES, `pt` đó sẽ *"không bị ảnh hưởng"* bởi: - AddRoundKey: chắc chắn rồi vì chỉ là xor thôi - SubBytes: vì tính song ánh của ánh xạ - ShiftRows: bản chất chỉ là dịch vị trí, không quá quan trọng tuy nhiên đến bước MixColumns thì câu chuyện đã khác. Bản chất của MixColumns là nhân ma trận trong trường Galois 256, khi đó một byte `index` kia đã làm thay đổi cả một `pt`. Nhưng nếu dừng lại ở đây thì đã không có `Square Attack` để dùng, mà một tính chất đặc biệt ta có thể rút ra là: > Nếu đem tất cả các `index` đó `xor` với nhau, kết quả sẽ đúng bằng `b'\x00'` (hiểu đơn giản là mình đã mã hóa toàn bộ 256 trường hợp của `index` thì `xor` lại sẽ ra `b'\x00'` thôi) - Tạm gọi tính chất này là `target`. Tính chất này chính là chìa khóa để giúp mình giải lại `Roundkey` cuối cùng rồi tìm lại `key`. Hình trên chỉ là 3 rounds đầu của AES, dưới đây là round cuối: ![image](https://hackmd.io/_uploads/B1zb7pMQA.png) - Vẫn sẽ là `pt` đó trải qua các bước sub, shift, add và không có mix, cụ thể mình sẽ khai thác ở bước add kia. Dựa vào tính chất `target` ở trên, nếu ta lấy tất cả `pt` đem `xor` với nhau và sau đó `xor` với `Roundkey` cuối cùng, ta sẽ được `res` chính xác là `Roundkey` đó luôn. Tới đây chúng ta chỉ cần bruteforce để tìm lại `Roundkey` tại vị trí `index` bằng cách phép `xor` thui :monkey:. - Hướng làm là đem tất cả các `ct` nhận được và `xor` với 1 byte trong không gian mẫu, tạm gọi là `guess`, cho `guess` đi qua hàm `Inv_ShiftRows` và `Inv_SubBytes` để tìm lại `index_guess`, lưu nó vào một mảng riêng rồi làm tương tự với các trường hợp còn lại. Nếu mảng đó thỏa tính chất `target` thì `guess` có thể là byte của `Roundkey` và chúng ta sẽ cố gắng tạo ra `pt` mà chỉ có đúng một trường hợp `guess` thỏa mãn, khi đó `guess` chắc chắn đúng (nghe cũng hơi giống giống `meet-in-the-middle attack` nhỉ :monkey:) ##### Tìm lại `key`: - Áp dụng thuật toán trên cho toàn bộ 16 bytes của `Roundkey`, mình có thể recover lại `Roundkey`, khi đó ném nó qua hàm build sẵn đâu đó trên github là tìm lại được `key` thui. ##### Full code: ```python= '''nc 103.163.24.78 2004''' import os from pwn import * from tqdm import * from aeskeyschedule import * HOST = '103.163.24.78' PORT = 2004 r = remote(HOST, PORT) print(r.recv().decode()) def encrypt(pt): r.sendlineafter(b'> ', b'1') r.sendlineafter(b': ', pt.hex().encode()) ct = r.recvuntil(b'\n').decode() return bytes.fromhex(ct) def gen_A_set(index): pt = os.urandom(16) A_set = [] pt = bytearray(pt) for i in range(256): pt[index] = i A_set.append(encrypt(pt)) return A_set def guess_roundkey(index): index_guess = set(list(range(256))) while True: A_set = gen_A_set(index) guess = set() for i in range(256): target = 0 for state in A_set: target ^= inv_sbox[state[index] ^ i] if target == 0: guess.add(i) index_guess.intersection_update(guess) if len(index_guess) == 1: return index_guess.pop() roundkey = [] for i in tqdm(range(16)): index = guess_roundkey(i) roundkey.append(index) key = reverse_key_schedule(bytes(roundkey), 4) r.sendlineafter(b'> ', b'2') r.sendlineafter(b': ', bytes.hex(key).encode()) print(r.recvline()) ``` ### Flag > ~~`KCSC{Sq4re_4tt4ck_ez_2_uNderSt4nd_4nD_1mPlement_R1ght?_:3}`~~ ### Note - Mình mất khá lâu để hiểu bài trên BlockBreakers nên trong wu này mình đã cố gắng viết lại theo hướng dễ hiểu nhất, follow hoàn toàn theo cấu trúc của AES và giải thích rõ ở mỗi bước tại sao lại như thế. Tuy nhiên cũng khá xứng đáng để bỏ thời gian ra hiểu một cách tấn công hay như thế hihi :smile_cat:. - Chạy ra flag mất 13p huhuhu :crying_cat_face:. --- ## 4. Don Copper (7 Solves) ### Description - Any crypto-er doesn't know Coppersmith technique can never be a true crypto-er - Challenge file: ```python= import random from Crypto.Util.number import getPrime NBITS = 2048 def pad(msg, nbits): """msg -> trash | 0x00 | msg""" pad_length = nbits - len(msg) * 8 - 8 assert pad_length >= 0 pad = random.getrandbits(pad_length).to_bytes((pad_length+7) // 8, "big") return pad + b"\x00" + msg if __name__ == '__main__': p = getPrime(NBITS//2) q = getPrime(NBITS//2) n = p*q e = 3 print('n =',n) flag = b'KCSC{s0m3_r3ad4ble_5tr1ng_like_7his}' flag1 = int.from_bytes(pad(flag[:len(flag)//2], NBITS-1), "big") flag2 = int.from_bytes(pad(flag[len(flag)//2:], NBITS-1), "big") print('c1 =', pow(flag1, e, n)) print('c2 =', pow(flag2, e, n)) print('c3 =', pow(flag1 + flag2 + 2024, e, n)) ''' n = 20309506650796881616529290664036466538489386425747108847329314416833872927305399144955238770343216928093685748677981345624111315501596571108286475815937548732237777944966756121878930547704154830118623697713050651175872498696886388591990290649008566165706882183536432074074093989165129982027471595363186012032012716786766898967178702932387828604019583820419525077836905310644900660107030935400863436580408288191459013552406498847690908648207805504191001496170310089546275003489343333654260825796730484675948772646479183783762309135891162431343426271855443311093315537542013161936068129247159333498199039105461683433559 c1 = 4199114785395079527708590502284487952499260901806619182047635882351235136067066118088238258758190817298694050837954512048540738666568371021705303034447643372079128117357999230662297600296143681452520944664127802819585723070008246552551484638691165362269408201085933941408723024036595945680925114050652110889316381605080307039620210609769392683351575676103028568766527469370715488668422245141709925930432410059952738674832588223109550486203200795541531631718435391186500053512941594901330786938768706895275374971646539833090714455557224571309211063383843267282547373014559640119269509932424300539909699047417886111314 c2 = 15650490923019220133875152059331365766693239517506051173267598885807661657182838682038088755247179213968582991397981250801642560325035309774037501160195325905859961337459025909689911567332523970782429751122939747242844779503873324022826268274173388947508160966345513047092282464148309981988907583482129247720207815093850363800732109933366825533141246927329087602528196453603292618745790632581329788674987853984153555891779927769670258476202605061744673053413682672209298008811597719866629672869500235237620887158099637238077835474668017416820127072548341550712637174520271022708396652014740738238378199870687994311904 c3 = 18049611726836505821453817372562316794589656109517250054347456683556431747564647553880528986894363034117226538032533356275073007558690442144224643000621847811625558231542435955117636426010023056741993285381967997664265021610409564351046101786654952679193571324445192716616759002730952101112316495837569266130959699342032640740375761374993415050076510886515944123594545916167183939520495851349542048972495703489407916038504032996901940696359461636008398991990191156647394833667609213829253486672716593224216112049920602489681252392770813768169755622341704890099918147629758209742872521177691286126574993863763318087398 ''' ``` ### Solution - Đây là bài đầu tiên mình va phải khi giải đề và đã ngốn của mình gần 5h đồng hồ chỉ để nháp về GCD của hàm hai ẩn. Về cơ bản, bài này không khó nhưng về lí thuyết toán lại quá phức tạp và trừu tượng. Nếu đã từng đọc về coppersmith, hẳn mọi người cũng biết tới dạng bài `coppersmith short pad attack` kết hợp với `franklin-reiter related attack` và lần đầu tiên khi đọc bài, ý nghĩ ập tới trong đầu là sử dụng GCD để tìm lại hai giá trị `flag1` và `flag2`. Tuy nhiên, hướng giải của bài không đơn giản đến thế vì cần tìm lại một trong hai `flag` thì mới recover lại được full, hint ở [đây](https://www.imo.universite-paris-saclay.fr/~pierre-loic.meliot/algebra/resultant.pdf) - Sau một hồi tìm tòi khá lâu thì mình kiếm được bài viết [này](https://en.wikipedia.org/wiki/System_of_polynomial_equations) để giải cách khác. Có thể thấy, nếu ta viết các phương trình trên thành như sau: ```= f1 = x^3 - c1 f2 = y^3 - c2 f3 = (x + y + 2024)^3 - c3 Với x, y là hai flag1 và flag2 ``` thì sẽ có ngay được path giải:![image](https://hackmd.io/_uploads/H1Pm_1fQ0.png) - Cơ bản thì cách giải vẫn sẽ là tìm nghiệm của phương trình một ẩn trước rồi thay vào tìm nghiệm còn lại của hệ, nhưng sẽ có một hướng khác. Đầu tiên chúng ta sẽ cần tìm cơ sở Grobner (Grobner basis) để việc tính toán đơn giản hơn. Với những bạn chưa tìm hiểu về cơ sở này thì đây là một công cụ khá mạnh để giải hệ phương trình đa thức. Tới đây mình sẽ thử đưa về Grobner basis trước để coi nhận được hệ phương trình mới ra sao (nhớ dùng sage): ```python= n = 20309506650796881616529290664036466538489386425747108847329314416833872927305399144955238770343216928093685748677981345624111315501596571108286475815937548732237777944966756121878930547704154830118623697713050651175872498696886388591990290649008566165706882183536432074074093989165129982027471595363186012032012716786766898967178702932387828604019583820419525077836905310644900660107030935400863436580408288191459013552406498847690908648207805504191001496170310089546275003489343333654260825796730484675948772646479183783762309135891162431343426271855443311093315537542013161936068129247159333498199039105461683433559 c1 = 4199114785395079527708590502284487952499260901806619182047635882351235136067066118088238258758190817298694050837954512048540738666568371021705303034447643372079128117357999230662297600296143681452520944664127802819585723070008246552551484638691165362269408201085933941408723024036595945680925114050652110889316381605080307039620210609769392683351575676103028568766527469370715488668422245141709925930432410059952738674832588223109550486203200795541531631718435391186500053512941594901330786938768706895275374971646539833090714455557224571309211063383843267282547373014559640119269509932424300539909699047417886111314 c2 = 15650490923019220133875152059331365766693239517506051173267598885807661657182838682038088755247179213968582991397981250801642560325035309774037501160195325905859961337459025909689911567332523970782429751122939747242844779503873324022826268274173388947508160966345513047092282464148309981988907583482129247720207815093850363800732109933366825533141246927329087602528196453603292618745790632581329788674987853984153555891779927769670258476202605061744673053413682672209298008811597719866629672869500235237620887158099637238077835474668017416820127072548341550712637174520271022708396652014740738238378199870687994311904 c3 = 18049611726836505821453817372562316794589656109517250054347456683556431747564647553880528986894363034117226538032533356275073007558690442144224643000621847811625558231542435955117636426010023056741993285381967997664265021610409564351046101786654952679193571324445192716616759002730952101112316495837569266130959699342032640740375761374993415050076510886515944123594545916167183939520495851349542048972495703489407916038504032996901940696359461636008398991990191156647394833667609213829253486672716593224216112049920602489681252392770813768169755622341704890099918147629758209742872521177691286126574993863763318087398 P.<x, y> = PolynomialRing(Zmod(n)) f1 = x^3 - c1 f2 = y^3 - c2 f3 = (x + y + 2024)^3 - c3 I = ideal(f1, f2, f3) # để dùng grobner cần phải đưa về ideal trước eq = I.groebner_basis() print(eq) ``` Nhận được: ```python= [x + 13735276216480234294706359844820346892092177215553901414391761850858882460635783205472406746662036273971992070607261796127119688947318559763272879567995396491115716121316753574975647148161690034108438505248524970885551589159652876676870846257287752279898736691564266993215011110767376345954955541741948634680315084968263393711859569144349753495551546569343780951427789448111710124904988309966385084377510291750050932987581798768855247780396174181337264410175133260701357189505301070362322065230989981704250274145256779438951189476596306356433095883900881070769915283761198556147219781991864498300605360624190735499303, y + 10850063064215786153306327148990924788438984598023598141431813572034023665508993022819406328531276035415236463762473907444014785919774345550388745382570351421234382216484857722363728432223575478904468786551137995573395304260701532033474455590992994546092536998679869622456414388329561594251029715255159436038660362296817381481412123357555319521172263012404519048712465622333028074769426400066439085668811357134225445548178218655872653355541146025686046192241356344100333096851330334054858073508194708777534526473121314794434611660370071725086052980875155623377312829194652529259245234592714291772331661063675437706202] ``` - Tới đây thì công việc đơn giản rồi, chúng ta sẽ phải giải hệ phương trình bậc nhất như trên (nhớ là trong trường `mod n`) và đó chính là `flag1` và `flag2` cần tìm đó. Full source: ```python= from Crypto.Util.number import long_to_bytes n = 20309506650796881616529290664036466538489386425747108847329314416833872927305399144955238770343216928093685748677981345624111315501596571108286475815937548732237777944966756121878930547704154830118623697713050651175872498696886388591990290649008566165706882183536432074074093989165129982027471595363186012032012716786766898967178702932387828604019583820419525077836905310644900660107030935400863436580408288191459013552406498847690908648207805504191001496170310089546275003489343333654260825796730484675948772646479183783762309135891162431343426271855443311093315537542013161936068129247159333498199039105461683433559 c1 = 4199114785395079527708590502284487952499260901806619182047635882351235136067066118088238258758190817298694050837954512048540738666568371021705303034447643372079128117357999230662297600296143681452520944664127802819585723070008246552551484638691165362269408201085933941408723024036595945680925114050652110889316381605080307039620210609769392683351575676103028568766527469370715488668422245141709925930432410059952738674832588223109550486203200795541531631718435391186500053512941594901330786938768706895275374971646539833090714455557224571309211063383843267282547373014559640119269509932424300539909699047417886111314 c2 = 15650490923019220133875152059331365766693239517506051173267598885807661657182838682038088755247179213968582991397981250801642560325035309774037501160195325905859961337459025909689911567332523970782429751122939747242844779503873324022826268274173388947508160966345513047092282464148309981988907583482129247720207815093850363800732109933366825533141246927329087602528196453603292618745790632581329788674987853984153555891779927769670258476202605061744673053413682672209298008811597719866629672869500235237620887158099637238077835474668017416820127072548341550712637174520271022708396652014740738238378199870687994311904 c3 = 18049611726836505821453817372562316794589656109517250054347456683556431747564647553880528986894363034117226538032533356275073007558690442144224643000621847811625558231542435955117636426010023056741993285381967997664265021610409564351046101786654952679193571324445192716616759002730952101112316495837569266130959699342032640740375761374993415050076510886515944123594545916167183939520495851349542048972495703489407916038504032996901940696359461636008398991990191156647394833667609213829253486672716593224216112049920602489681252392770813768169755622341704890099918147629758209742872521177691286126574993863763318087398 P.<x, y> = PolynomialRing(Zmod(n)) f1 = x^3 - c1 f2 = y^3 - c2 f3 = (x + y + 2024)^3 - c3 I = ideal(f1, f2, f3) # để dùng grobner cần phải đưa về ideal trước eq = I.groebner_basis() flag1 = int(x - eq[0]) flag2 = int(y - eq[1]) flag1 = long_to_bytes(flag1).split(b'\x00')[-1] flag2 = long_to_bytes(flag2).split(b'\x00')[-1] flag = flag1 + flag2 print(flag) ``` ### Flag > ~~`KCSC{W0rk1ng_w1th_p0lyn0m14ls_1s_34sy_:D}`~~ ### Note - Đối với cách làm của resultant, sau khi tham khảo tùm lum tùm la, mình đã biết làm hehe. Thực ra cách làm của mình chỉ cần yêu cầu tìm một `flag1` hoặc `flag2` là đủ, rồi theo hướng `franklin-reiter` kia mà phang ra thằng còn lại thôi. - Trước tiên cần hiểu `resultant` là gì. `Resultant` của hai đa thức có thể được coi là `Eliminant` của hai đa thức đó (không biết dịch bằng nhân tử có đúng không) nhưng trên hết nó sẽ giúp ta đưa từ đa thức nhiều ẩn về ít ẩn hơn (với thuật toán thì tạm thời mình chưa giải thích vì nó khá trừu tượng và lằng nhằng). Bằng việc sử dụng `resultant` kết hợp với tìm `gcd` của hai đa thức ta có thể tìm lại `flag` (khá hay đúng khum :monkey:): ```python= from Crypto.Util.number import long_to_bytes n = 20309506650796881616529290664036466538489386425747108847329314416833872927305399144955238770343216928093685748677981345624111315501596571108286475815937548732237777944966756121878930547704154830118623697713050651175872498696886388591990290649008566165706882183536432074074093989165129982027471595363186012032012716786766898967178702932387828604019583820419525077836905310644900660107030935400863436580408288191459013552406498847690908648207805504191001496170310089546275003489343333654260825796730484675948772646479183783762309135891162431343426271855443311093315537542013161936068129247159333498199039105461683433559 c1 = 4199114785395079527708590502284487952499260901806619182047635882351235136067066118088238258758190817298694050837954512048540738666568371021705303034447643372079128117357999230662297600296143681452520944664127802819585723070008246552551484638691165362269408201085933941408723024036595945680925114050652110889316381605080307039620210609769392683351575676103028568766527469370715488668422245141709925930432410059952738674832588223109550486203200795541531631718435391186500053512941594901330786938768706895275374971646539833090714455557224571309211063383843267282547373014559640119269509932424300539909699047417886111314 c2 = 15650490923019220133875152059331365766693239517506051173267598885807661657182838682038088755247179213968582991397981250801642560325035309774037501160195325905859961337459025909689911567332523970782429751122939747242844779503873324022826268274173388947508160966345513047092282464148309981988907583482129247720207815093850363800732109933366825533141246927329087602528196453603292618745790632581329788674987853984153555891779927769670258476202605061744673053413682672209298008811597719866629672869500235237620887158099637238077835474668017416820127072548341550712637174520271022708396652014740738238378199870687994311904 c3 = 18049611726836505821453817372562316794589656109517250054347456683556431747564647553880528986894363034117226538032533356275073007558690442144224643000621847811625558231542435955117636426010023056741993285381967997664265021610409564351046101786654952679193571324445192716616759002730952101112316495837569266130959699342032640740375761374993415050076510886515944123594545916167183939520495851349542048972495703489407916038504032996901940696359461636008398991990191156647394833667609213829253486672716593224216112049920602489681252392770813768169755622341704890099918147629758209742872521177691286126574993863763318087398 P.<x, y> = PolynomialRing(ZZ, 2) f1 = x^3 - c1 f2 = y^3 - c2 f3 = (x + y + 2024)^3 - c3 a = f3.resultant(f1) b = f3.resultant(f2) a, b = a.change_ring(Zmod(n)), b.change_ring(Zmod(n)) flag2 = int(y-gcd(a, b)) # Franklin-Reiter PR.<X> = PolynomialRing(Zmod(n)) g1 = (X)^3 - c1 g2 = (X + flag2 + 2024)^3 - c3 while g2: g1, g2 = g2, g1 % g2 flag1 = int(-g1.monic()[0]) flag1 = long_to_bytes(flag1).split(b'\x00')[-1] flag2 = long_to_bytes(flag2).split(b'\x00')[-1] flag = flag1 + flag2 print(flag) ```