Tags: cryptography exclusion key-recovery 

Rating: 5.0

## 1. An interesting solution (read first)

This challenge generates a 32-byte random key.
```
key = os.urandom(32)
```
It then encrypts the flag, and returns the result.
```
def __init__(self):
print(WELCOME + f"Here is the encrypted flag : {encrypt(FLAG).hex()}")
```
The user has two choices: encrypt and decrypt. The decrypt choice accepts a key and ciphertext, and checks whether "FwordCTF" occurs in the decrypted text.
```
elif c == '2':
k = bytes.fromhex(input("\nKey : "))
cipher = bytes.fromhex(input("Ciphertext : "))
flag = decrypt(cipher, k)
if b"FwordCTF" in flag:
print(f"Well done ! Here is your flag : {FLAG}")
```
The decrypt function performs simple AES decryption, but it then xors the resulting ciphertext with the key.
```
def decrypt(cipher, k):
aes = AES.new(k, AES.MODE_ECB)
cipher = xor(cipher, k)
msg = unpad(aes.decrypt(cipher), 16)
return msg
```
For the record, the encrypt function does the same xor, but it doesn't matter for the solution.
```
def encrypt(msg):
aes = AES.new(key, AES.MODE_ECB)
if len(msg) % 16 != 0:
msg = pad(msg, 16)
cipher = aes.encrypt(msg)
cipher = xor(cipher, key)
return cipher
```
The encrypt choice will just encrypt random bytes. It will then return "Something seems leaked" when one of the bytes corresponds with a byte from the key.
```
if c == '1':
msg = os.urandom(32)
cipher = encrypt(msg)
if all(a != b for a, b in zip(cipher, key)):
print(cipher.hex())
else:
print("Something seems leaked !")
```
One can consecutively call the encrypt function to get random bytes that are not in the key. It's like brute-forcing every key byte at the same time. After retrieving 255 different bytes for every character in the key, that is not in the key, it's trivial to know the key, since it's composed of the last bytes standing. Then, you submit the key + encrypted flag and the flag is returned (or you decrypt it yourself).
```
from pwn import *
import binascii

HOST = "52.149.135.130"
PORT = 4869

notKeyMapping = {}
for i in range(32):
notKeyMapping[i] = []

r = remote(HOST, PORT)

initialResponse = r.recvuntil(b'> ')
encryptedFlag = initialResponse.split()[10].split(b'\\n')[0]
print("[+] Encrypted flag: %s" % encryptedFlag)
rawEncryptedFlag = binascii.unhexlify(encryptedFlag)

def hasAllValues(notKeyMapping):
for i in notKeyMapping:
if len(notKeyMapping[i]) < 255:
return False
return True

def getValuesLeft(notKeyMapping):
left = 0
for i in notKeyMapping:
if len(notKeyMapping[i]) < 255:
left += 255 - len(notKeyMapping[i])
return left

while not hasAllValues(notKeyMapping):

r.sendline('1')
response = r.recvuntil(b'> ')

if b'Something seems leaked' in response:
continue

else:
notKey = response.split(b'\n')[0]
parsedNotKey = binascii.unhexlify(notKey)

for i,v in enumerate(parsedNotKey):
if v not in notKeyMapping[i]:
notKeyMapping[i].append(v)

valuesLeft = getValuesLeft(notKeyMapping)
if valuesLeft % 500 == 0:
print("[+] Values left: %s" % getValuesLeft(notKeyMapping))

key = []
for i in range(32):
for c in range(256):
if c in notKeyMapping[i]:
continue
else:
key.append(c)

keyBytes = bytes(key)

r.sendline('2')
r.sendline(keyBytes.hex())
r.sendline(rawEncryptedFlag.hex())

r.interactive()
```

![Flag](https://i.imgur.com/GYAW2k1.png)

I believe this was the way the challenge was meant to be solved.

## 2. Lame solution
Those who have been paying attention might have seen a different solution, which is rather lame. You can just create a valid ciphertext/key combination by encrypting "FwordCTF" with the encrypt function, but where is the fun in that:

```
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

key = b"A"*32

def encrypt(msg):
aes = AES.new(key, AES.MODE_ECB)
if len(msg) % 16 != 0:
msg = pad(msg, 16)
cipher = aes.encrypt(msg)
cipher = xor(cipher, key)
return cipher

encrypted = encrypt(b"FwordCTF")

HOST = "52.149.135.130"
PORT = 4869

r = remote(HOST, PORT)
initialResponse = r.recvuntil(b'> ')

r.sendline('2')
r.sendline(key.hex())
r.sendline(encrypted.hex())

r.interactive()
```