We are given a small AES-CTR encryption service:
secret_key = os.urandom(16)
def encrypt(plaintext, counter):
m = hashlib.sha256()
m.update(counter.to_bytes(8, byteorder="big"))
alg = AES.new(secret_key, AES.MODE_CTR, nonce=m.digest()[0:8])
ciphertext = alg.encrypt(plaintext)
return ciphertext.hex()
The service lets us send one plaintext and then encrypts it 256 times, once for each counter value from
That means the flag nonce is not really random enough. It must be one of the 256 nonces we can already ask the service to use.
For CTR mode, encryption is just:
ciphertext = plaintext XOR keystream
So if we send a plaintext consisting only of zero bytes, the returned ciphertext is the keystream itself. We can collect all 256 possible keystreams and then try each of them against the encrypted flag.
from pwn import *
sample_encrypted_flag = "7a6ee510bd15ddb026e6b4f8114109133331865d5749d5a60b3c5d36a81dc048293ff294f79ce779d56787f9a88a773ac97d72a217d18ebfd1cc30b139b2dd9c53aa0ab97012"
remote = remote(
"jvfsfjh7dookpwyd3hbtd2tigu-1024-intro-crypto-1.challenge.cscg.live",
443,
ssl=True,
)
remote.recvuntil(b"Enter some plaintext (hex): ")
remote.sendline(b"0" * len(sample_encrypted_flag))
keystreams = []
for i in range(256):
remote.recvuntil(f"Ciphertext {i:03d}: ".encode())
keystreams.append(bytes.fromhex(remote.recvline().strip().decode()))
remote.recvuntil(b"Flag: ")
encrypted_flag = bytes.fromhex(remote.recvline().strip().decode())
for keystream in keystreams:
possible_flag = bytes(a ^ b for a, b in zip(encrypted_flag, keystream))
text = possible_flag.decode(errors="ignore")
if "CSCG{" in text:
print(text)
Running this script gives us the flag:
CSCG{CTR_A3S_Br0ken!???N0pe,it's_C4ll3d_number_used_0nce_f0r_a_r3as0n}