# Crypto - Still no decrypt ![](https://i.imgur.com/E1FXNjh.png) 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. ![](https://i.imgur.com/AlE64L2.png) We can run the program locally to figure out what happened. ![](https://i.imgur.com/wQOo9lP.png) 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). ![](https://i.imgur.com/UvWwCtx.png) 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. ![](https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/CBC_decryption.svg/2880px-CBC_decryption.svg.png) 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} ``` ![](https://i.imgur.com/z0Icbv2.png)