---
# System prepended metadata

title: Crypto - Still no decrypt

---

# 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)
