UMass CTF 2026 - The Accursed Lego Bin

I dropped my message into the bin of Legos. It’s all scrambled up now. Please help.

Initial Analysis

The challenge provides a Python script encoder.py and its corresponding output output.txt. The goal is to recover a flag that has had its bits scrambled based on a seed derived from a hardcoded string.

Seed Generation

The script uses RSA to “encrypt” the string "I_LOVE_RNG":

1
2
3
4
text = "I_LOVE_RNG"
n, seed = RSA_enc(text)
# ...
enc_seed = pow(seed, e, n)

The RSA_enc function calculates seed = pow(plain_num, e, n). In the main function, it then calculates enc_seed = pow(seed, e, n). Since e = 7 is very small and the plaintext "I_LOVE_RNG" is short, the value me (where m is the integer representation of the text) is much smaller than the 4096-bit RSA modulus n. This means pow(m, e, n) is simply me without any modular reduction.

Bit Shuffling

1
2
3
4
flag_bits = get_flag_bits(flag)
for i in range(10):
random.seed(seed*(i+1))
random.shuffle(flag_bits)

The flag is converted into a list of bits and shuffled 10 times using Python’s random.shuffle. Each shuffle is seeded with a value derived from the seed identified above.

Solution

To recover the flag, we follow these steps:

  1. Recover the Seed: Calculate the integer value of "I_LOVE_RNG" and raise it to the 7th power to get the seed. We can verify this against enc_seed in the output file by checking if seed**7 == enc_seed.
  2. Reverse the Shuffle: Since random.shuffle is deterministic when the seed is known, we can track how the positions change. Instead of shuffling the bits directly, we shuffle a list of indices [0, 1, 2, ..., len(bits)-1].
  3. Reconstruct the Flag: After 10 shuffles, the index list tells us where each original bit ended up. We map the bits from the encoded flag back to their original positions and convert the bit array back into a string.

Solution 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
import random

def get_flag_bits(flag_hex):
flag_bytes = bytes.fromhex(flag_hex)
bits = []
for b in flag_bytes:
bits.extend(list(bin(b)[2:].zfill(8)))
return bits

def bit_arr_to_str(bit_arr):
byte_arr = []
for i in range(0, len(bit_arr), 8):
byte = bit_arr[i:i+8]
char = int(''.join(byte), 2)
byte_arr.append(char)
return bytes(byte_arr)

# Values from output.txt
enc_flag_hex = "a9fa3c5e51d4cea498554399848ad14aa0764e15a6a2110b6613f5dc87fa70f17fafbba7eb5a2a5179"
text = "I_LOVE_RNG"
plain_num = int.from_bytes(text.encode(), "big")
seed = plain_num ** 7

flag_bits_enc = get_flag_bits(enc_flag_hex)
num_bits = len(flag_bits_enc)
indices = list(range(num_bits))

# Replay the shuffle on the indices
for i in range(10):
random.seed(seed * (i + 1))
random.shuffle(indices)

# Map scrambled bits back to original positions
flag_bits_orig = [None] * num_bits
for j in range(num_bits):
flag_bits_orig[indices[j]] = flag_bits_enc[j]

print(bit_arr_to_str(flag_bits_orig).decode())

Flag

UMASS{tH4Nk5_f0R_uN5CR4m8L1nG_mY_M3554g3}