AmateursCTF 2025 | Write-up of crypto challenges
| Challenge | Tags | Points | Solves |
|---|---|---|---|
| aescure | AES | 50 | 126 |
| uncrackable | One Time Pad, RNG | 58 | 96 |
| division | LLL | 162 | 53 |
| triangulate | LCG | 200 | 42 |
aescure
Points : 50
Solves : 126
Description :
this is definitely not secure but im doing it anyways
Here is the source file of the challenge :
1
2
3
4
5
6
7
8
9
from Crypto.Cipher import AES
cipher = AES.new(open('flag.txt', 'rb').read(), AES.MODE_ECB)
pt = b'\x00' * 16
print(cipher.encrypt(pt).hex())
"""
5aed095b21675ec4ceb770994289f72b
"""
At first glance you might think: wtf it’s impossible, AES is resistant against known plaintext attacks.
But once you remember there is a flag format of 13 characters : amateursCTF{}, the chal becomes much easier. In fact, all you need to do is iterate over every possible flag (search space is small because it’s ascii) and stop when b"\x00"*16 is encrypted into 5aed095b21675ec4ceb770994289f72b.
uncrackable
Points : 58
Solves : 96
Description :
unless you have a supercomputer my messages should be safe
This challenge involve a One Time Pad :
1
2
3
4
5
6
7
8
9
10
def encrypt(x):
return xor(x := x.strip(), rng.get_bytes(len(x)))
rng = stream()
open("out.txt", "w").write(
b''.join(
encrypt(os.urandom(2)) for _ in range(10000)
).hex() + encrypt(flag).hex()
)
And here is the implementation of the rng :
1
2
3
4
5
6
7
8
9
10
11
12
class stream():
def __init__(self, seed=os.urandom(8)):
self.state = hashlib.sha256(str(seed).encode())\
.digest()[:len(flag)]
def next(self):
out = self.state[0]
self.state = self.state[1:] + bytes([(out + 1) % 256])
return out
def get_bytes(self, num):
return bytes(self.next()for _ in range(num))
The initial state is the sha256 of a 8-bytes seed, giving a 32 bytes uniformly distributed string. The next function is poorly designed: it outputs the first byte of the state, then appends that byte—after incrementing it mod 256—to the end of the state. But using only this flaw we cannot recover the flag, we do not have enough known plaintext.
The trick lies in the encrypt function : using strip on the output of urandom introduces a bias in the plaintext generation. Specifically, the characters : b" \n\r\t\x0b\x0c" will never be encrypted.
Let :
- $c$ the ciphertext and $c_i$ it’s $i$-th bytes.
- $s$ the 32 bytes state and $s_i$ it’s $i$-th bytes.
- $f_i$ the $i$-th forbidden plaintext character.
Knowing this trick, we see that $c_0$ cannot be, for $1 \le j \le 6$, equal to $f_j \oplus s_0$ (because it’s forbidden characters). This implies that $s_0$ can’t be equal to $c_i \oplus f_j$. Doing this for all $j$ (i.e., for $1 \le j \le 6$) yields six forbidden values for $s_0$. We’ve reduced the key space, great ! But we can go further.
Now, let’s jump to $c_{i+l}$ (with $l$ being the state length) and repeat the process to introduces six more forbidden keys. Note that our previous forbidden keys must be incremented by 1 to remain forbidden for $c_{i+l}$; this follows from how the state update works. We eliminate 12 possibilities for the first bytes of the state, we have to do it again until only one possibilities remains—this is possible because we have a looong ciphertext.
Repeating this for every byte of the state enable us to fully recover it—and to retrieve the flag.
division
Points : 162
Solves : 53
Description :
they said i could just use division to find the flag but something’s up
TODO
triangulate
Points : 200
Solves : 42
Description :
if i give you a triangular number of ‘triangular’ outputs then that will help you triangulate the flag right
TODO
