Table of Contents


X3CTF 2025 Writeup

X3CTF 2025 Writeup

Solutions for selected challenges from X3CTF 2025.

X3CTF 2025 Writeup

A collection of write-ups for challenges from X3CTF 2025.

notcrypto

Author: dagurb • Solves: 111 Description: You shouldn’t need to call your crypto teammate for this challenge lol.

Solution: This challenge involves reversing a custom block cipher-like operation. The solution implements the inverse of the transformation applied to an 8-byte block over 4096 rounds, using a pre-computed inverse substitution box. The flag is revealed after reversing all blocks and removing PKCS#7 padding.

byte_4050 = [
  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
]
byte_2010 = [
  0x16, 0x2D, 0x79, 0xCA, 0x56, 0xC6, 0x65, 0xE9, 0xE9, 0x16, 
  0x66, 0x23, 0x09, 0x2D, 0x1B, 0x09, 0x1C, 0x09, 0xC6, 0x1C, 
  0x1F, 0xAD, 0xE9, 0xDA, 0xA0, 0xC6, 0x1A, 0x66, 0x09, 0xAD, 
  0x81, 0x1C, 0x80, 0x39, 0xA0, 0x21, 0x09, 0x65, 0x2D, 0x30, 
  0xF6, 0x57, 0xF6, 0xA2, 0x65, 0x65, 0x21, 0xA2, 0x78, 0x78, 
  0x78, 0x78, 0x78, 0x78, 0x78, 0x78
]
def reverse_block(block, inv_4050):
    state = list(block)
    for i in reversed(range(4096)):
        i_mod = i % 256
        new_v13, new_v14, new_v15, new_v16, new_v17, new_v18, new_v19, new_v20 = state
        
        old_v13 = inv_4050[(new_v14 ^ i_mod)]
        old_v14 = inv_4050[(new_v17 ^ i_mod)]
        old_v15 = inv_4050[(new_v19 ^ i_mod)]
        old_v16 = inv_4050[(new_v16 ^ i_mod)]
        old_v17 = inv_4050[(new_v20 ^ i_mod)]
        old_v18 = inv_4050[(new_v18 ^ i_mod)]
        old_v19 = inv_4050[(new_v13 ^ i_mod)]
        old_v20 = inv_4050[(new_v15 ^ i_mod)]
        
        state = [old_v13, old_v14, old_v15, old_v16, old_v17, old_v18, old_v19, old_v20]
    return bytes(state)

inv_4050 = [0] * 256
for x in range(256):
    inv_4050[byte_4050[x]] = x

reversed_blocks = []
for i in range(0, 56, 8):
    block = byte_2010[i:i+8]
    reversed_block = reverse_block(block, inv_4050)
    reversed_blocks.append(reversed_block)

padded_flag = b''.join(reversed_blocks)
pad_len = padded_flag[-1]
flag = padded_flag[:-pad_len].decode('ascii', errors='ignore')
print(flag)

Flag: x3c{pwndbg_and_pwntools_my_belowed_573498532832}

oh-my-gadt

Author: natan.p • Solves: 22 Description: We had our unpaid intern make an uncrackable key checker. They quit because of “inadequate pay”, but they took the source code and the keys with them. We really need these keys!

Solution: This challenge requires reversing a complex multi-stage transformation applied to a large integer. The solution systematically reverses 13 rounds of four distinct transformations:

  1. Reversing a chunk rotation.
  2. Reversing a non-linear byte transformation using a pre-computed inverse map.
  3. Reversing a differential XOR operation.
  4. Reversing a circular shift and XOR mixing. By applying these inverse functions in the correct order, the original bytes of the flag are recovered.
from collections import defaultdict

def number_to_bytes(n):
    bytes_list = []
    while n > 0:
        bytes_list.append(n % 256)
        n = n // 256
    bytes_list.reverse()
    return bytes_list

n = 2205967053642207131367982253372196254666549571698892523008302353266425115464942068759663110459718064363978392428938015071
bytes_list = number_to_bytes(n)

reverse_map = defaultdict(list)
for x in range(256):
    y = (6 * (x**6) + 2 * (x**3) + x) % 256
    reverse_map[y].append(x)

def reverse_t4(bytes_list):
    for _ in range(3):
        if not bytes_list:
            continue
        xor_prev = 0
        for b in bytes_list[:-1]:
            xor_prev ^= b
        a = bytes_list[-1] ^ xor_prev
        bytes_list = [a] + bytes_list[:-1]
    return bytes_list

def reverse_t3(transformed):
    if not transformed:
        return []
    original = [transformed[-1]]
    for i in range(len(transformed) - 1):
        original.append(transformed[i] ^ transformed[i+1])
    return original

def reverse_t2(bytes_list):
    original = []
    for y in bytes_list:
        possible_x = reverse_map.get(y, [])
        possible_x_filtered = [x for x in possible_x if 32 <= x <= 126]
        if not possible_x_filtered:
            possible_x_filtered = possible_x
        if not possible_x_filtered:
            possible_x_filtered = [0]
        original.append(possible_x_filtered[0])
    return original

def reverse_t1(bytes_list):
    chunk_size = 4
    chunks = [bytes_list[i:i+chunk_size] for i in range(0, len(bytes_list), chunk_size)]
    original_chunks = []
    for i, chunk in enumerate(chunks):
        if len(chunk) < chunk_size:
            original_chunks.append(chunk)
            continue
        rotate_by = i % chunk_size
        rotated = chunk[-rotate_by:] + chunk[:-rotate_by] if rotate_by != 0 else chunk
        original_chunks.append(rotated)
    original = []
    for chunk in original_chunks:
        original.extend(chunk)
    return original

for _ in range(13):
    bytes_list = reverse_t4(bytes_list)
    bytes_list = reverse_t3(bytes_list)
    bytes_list = reverse_t2(bytes_list)
    bytes_list = reverse_t1(bytes_list)

flag = bytes(bytes_list).decode('ascii', errors='ignore')
print("Flag:", flag)

Flag: MVM{::333:3:/::33::33/w0w_g4d75_n_ph4n70m5_y1pp33}

pickle-season

Author: hackrrr • Solves: 80 Description: It is that silly pickle season again… Nothing happens, no CTF challenges, just boredom. And so I created this challenge to fight that and help you to get through this season. I just hope you are not afraid of pickles.

Solution: This challenge involves reversing a simple XOR chain. The solution reconstructs the original characters by iteratively XORing the previous byte with the current target byte. The final step involves reversing the entire recovered string to get the flag.

targets = [
    674, 716, 764, 655, 699, 648, 763, 676, 663, 763,
    656, 755, 706, 658, 717, 675, 658, 717, 672, 656,
    756, 711, 693, 645, 711, 700, 753, 679, 746
]

targets_bytes = [t % 256 for t in targets]

initial_byte = (-42) % 256  # 214

prev = initial_byte
chars = []
for tb in targets_bytes:
    char = prev ^ tb
    chars.append(char)
    prev = tb

flag = ''.join(chr(c) for c in chars)

print(flag[::-1])

Flag: MVM{B0r3d0m_1n_P1ckl3_s34s0nt}