# Crypto - Still no decrypt

We are given this python program and a service where the program is ran.
```python=
#!/usr/bin/env python3
from Crypto.Cipher import AES
from Crypto.Random import urandom
from cmd import Cmd
key = urandom(32)
with open('flag.txt') as f:
flag = f.read().strip()
assert len(flag) == 15
assert flag.startswith('S2G')
assert flag.endswith('}')
def pad(pt):
return pt + b'#'*((16-len(pt)) % AES.block_size)
def unpad(pt):
return pt.rstrip(b'#')
def encrypt(pt):
cipher = AES.new(key, AES.MODE_CBC)
return cipher.iv + cipher.encrypt(pad(pt.encode()))
def decrypt(ct):
cipher = AES.new(key, AES.MODE_CBC, iv=ct[:16])
return unpad(cipher.decrypt(ct[16:])).decode()
class Encrypter(Cmd):
'''Encrypt or decrypt data'''
intro = 'Welcome to Still no decrypt! Type help to get started.'
prompt = '>>> '
def do_encrypt(self, line):
'''Encrypt a plaintext'''
if line == 'S2G':
plaintext = 'System Security research group'
elif line == 'CTF':
plaintext = 'Capture the Flag'
elif line == 'NTNU':
plaintext = 'Norges teknisk-naturvitenskapelige universitet'
elif line == 'flag':
plaintext = flag
else:
plaintext = ':)'
ciphertext = encrypt(plaintext).hex()
print(f'The ciphertext is {ciphertext}')
def do_decrypt(self, line):
'''Decrypt a ciphertext'''
plaintext = decrypt(bytes.fromhex(line))
# print(f'The plaintext is {plaintext}')
# :(
Encrypter().cmdloop()
```
We can encrypt multiple texts using [AES CBC](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC)), one of which is the flag, and get their ciphertext. We are also able to decrypt any ciphertext of our choice, but we are not getting any output.
We can see the flag is in this format `S2G{XXXXXXXXXX}` (except the `{`, which was accidentally missing).
```python
with open('flag.txt') as f:
flag = f.read().strip()
assert len(flag) == 15
assert flag.startswith('S2G')
assert flag.endswith('}')
```
The key is a random value.
```python
key = urandom(32)
```
The encryption function should be safe. We are just allowed to encrypt predefined values.
```python
def encrypt(pt):
cipher = AES.new(key, AES.MODE_CBC)
return cipher.iv + cipher.encrypt(pad(pt.encode()))
```
We will not be able to get anything from the padding functions, such as [padding oracle attacks](https://en.wikipedia.org/wiki/Padding_oracle_attack), since they won't throw any errors upon invalid padding.
```python
def pad(pt):
return pt + b'#'*((16-len(pt)) % AES.block_size)
def unpad(pt):
return pt.rstrip(b'#')
```
Looking at the decrypt function, we can see it is using our provided input of an IV and ciphertext.
```python
def decrypt(ct):
cipher = AES.new(key, AES.MODE_CBC, iv=ct[:16])
return unpad(cipher.decrypt(ct[16:])).decode()
def do_decrypt(self, line):
'''Decrypt a ciphertext'''
plaintext = decrypt(bytes.fromhex(line))
# print(f'The plaintext is {plaintext}')
# :(
```
We can try to decrypt a few values. Decrypting given ciphertexts of the flag will not return anything. Decrypting a random ciphertext will close the socket.

We can run the program locally to figure out what happened.

The `.decode()` in the decrypt function is throwing an error and crashing the program, because of invalid [UTF-8](https://en.wikipedia.org/wiki/UTF-8).

We can use this as an oracle to recover the plaintext of the flag.
By modifying the IV we can test each character to validate if it is the correct character of the plaintext, by checking if they produce valid multibyte UTF-8 values.

We know the last 2 characters of the flag's plaintext are `}#` (last characeter of the flag + padding), so we will test each possible character of the previous character, to find the character that doesn't crash the program. By xoring the IV with the plaintexts characters themself (which cancels them out) and valid UTF-8 3-byte values, we will end up with a plaintext containing valid UTF-8. If we use the wrong character however, some of the UTF-8 combinations will become invalid. By following this method for every character of the flag, we can recover the whole flag.
Solve script:
```python=
#!/usr/bin/env python3
from pwn import *
warnings.filterwarnings("ignore")
HOST = '10.212.138.23'
PORT = 33458
# Find the valid UTF-8 3-byte values
valid_utf8_triplets = []
for i in range(128, 256):
for j in range(128, 256):
for k in range(128, 256):
try:
bytes([i,j,k]).decode()
valid_utf8_triplets.append((i,j,k))
except:
pass
# Not necessary to check all of them
valid_utf8_triplets = valid_utf8_triplets[::200]
def connect():
return remote(HOST, PORT, level='error')
def oracle(xor_patterns):
r = connect()
r.sendlineafter(b'>>> ', b'encrypt flag')
iv_ciphertext = bytes.fromhex(r.recvline(keepends=False).split()[3].decode())
try: # Tests if decrypt works
for xor_pattern in xor_patterns:
data = xor(iv_ciphertext, xor_pattern).hex()
r.sendlineafter(b'>>> ', f'decrypt {data}'.encode())
r.sendlineafter(b'>>> ', b'encrypt')
r.recvline()
r.close()
return True
except: # Program crashed
r.close()
return False
def recover_flag():
flag = '}#'
# Recover the characters of the flag backwards
for i in range(15-len(flag), -1, -1):
charset = string.digits + string.ascii_lowercase + string.ascii_uppercase
charset += ''.join([c for c in string.printable if c not in charset])
# Test each character
for c in charset.encode():
progress.status(chr(c) + flag)
# Create patterns to xor the IV with
xor_patterns = [
b'\x00'*i + bytes([ # pad before
c ^ triplet[0], # character to test
ord(flag[0]) ^ triplet[1], # next character in flag
ord(flag[1]) ^ triplet[2] # 2nd next char in flag
]) + b'\x00'*(13-i+16) # pad after
for triplet in valid_utf8_triplets
]
# Found the correct character
if oracle(xor_patterns):
flag = chr(c) + flag
break
return flag
progress = log.progress('Flag')
flag = recover_flag()
progress.success(''.join(flag).rstrip('#'))
# S2G{U7F80R4Cl3}
```
