Post

DeconstruCT.F 2023 - BLURY [DELETED]

Note

This challenge has been deleted after the staff has been noticed that it was copied from an existing challenge. I decided to post it anyway as I had already written the writeup.

Description

  • Category: Cryptography

Mr. Charlie Ben Conor’s security algorithms have been repeatedly urged by the management team to include as much randomization as possible. We believe he lacks understanding of conventional uses of symmetric cyphers, but he has always denied it owing to financial constraints. Can you demonstrate the potential consequences of his actions?

Attachments:

Hint

Rule 3 does not apply here. Bruteforce is allowed.

1. Overview

ciphertext.txt in encoded twice with base64 and contains partial ciphertext in hexadecimal:

1
54**************************36d43a**************************1658e7**************************bb2cdabc02a058f8c3936a5fd6ddd3c50491

encrypt.py gives us several piece of information:

  • the key is incomplete (missing 2 characters);
  • it uses AES CBC mode with the flag as IV to encrypt;
  • we also have the plaintext used for encryption.

2. Strategy

The goal is to recover the IV which is the flag.

Here is the equation to recover the IV:

\[C_{0} = Encrypt(IV \oplus P_{0}) \iff IV = Decrypt(C_{0}) \oplus P_{0}\]

Some notations:

  • $C_{k}$ is the $k$-th block of the ciphertext ($C_{k-1}=IV$);
  • $P_{k}$ is the $k$-th block of the plaintext;
  • $Encrypt$ and $Decrypt$ are associated AES operations.

However we are missing some characters in the first block of the ciphertext and the key is incomplete.

So in order to recover the flag (IV) we need to:

  1. Recover the key,
  2. Recover the first block of the ciphertext ($C_{0}$),

3. Recover the key

To recover the key, we can bruteforce the last two characters ($256^2$ possibilities).

But how can we verify if a key is the real key?

Despite the ciphertext is missing a lot of characters, we still have an entire block (the last block $C_{3}$):

1
dabc02a058f8c3936a5fd6ddd3c50491

Reminder: the size of a block is 16 bytes. Since 1 byte is encoded with 2 hex characters, the block size is 32 for hex strings.

And we also have some characters of the previous block ($C_{2}$):

1
e7**************************bb2c

Those information are enough te verify the key because according to this formula:

\[C_{3} = Encrypt(C_{2} \oplus P_{3}) \iff C_{2} = Decrypt(C_{3}) \oplus P_{3}\]

if $C_{2}$ (obtained by decrypting $C_{3}$ with a key $K$) starts with e7 and ends with bb2c (known characters of $C_{2}$), then $K$ is the valid key.

Here is the implementation in Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import base64
import binascii
from pwn import *
from Crypto.Cipher import AES

IV = b'\x00'*16

# Load data
plaintext = b"Security is not a joke, mind it. But complete security is a myth"
ciphertext = open("ciphertext.txt", "r").read()
ciphertext = base64.b64decode(ciphertext)
ciphertext = base64.b64decode(ciphertext).decode()

# Split in blocks
P = [plaintext[i:i+16] for i in range(0, len(plaintext), 16)]
C = [b'' for _ in range(len(ciphertext)//32)]

# Recover the key
C[3] = binascii.unhexlify(ciphertext[-32:])
partial_C2 = ciphertext[-64:-32].encode()

key = None
for i in range(255):
    for j in range(255):
        partial_key = b'3N7g309d6Y7enT' + bytes([i, j])
        cipher = AES.new(partial_key, AES.MODE_CBC, IV)
        C2 = xor(cipher.decrypt(C[3]), P[3])

        start_C2 = ciphertext[-64:-32].split('*')[0]
        end_C2 = ciphertext[-64:-32].split('*')[-1]
        
        # Verify with known characters of C2
        if C2.hex().startswith(start_C2) and C2.hex().endswith(end_C2):
            key = partial_key
            print("key recovered", key)
            break

    if key: break

And we recovered the key: key recovered b'3N7g309d6Y7enT1p'

4. Recover $C_{0}$

Now we have the key we can recover all the ciphertext:

\[C_{k+1} = Encrypt(C_{k} \oplus P_{k+1}) \iff C_{k} = Decrypt(C_{k+1}) \oplus P_{k+1}\]

where $C_{k-1}=IV$.

1
2
3
4
5
6
7
D = lambda s: AES.new(key, AES.MODE_CBC, IV).decrypt(s)

C[2] = xor( D(C[3]) , P[3] )
C[1] = xor( D(C[2]) , P[2] )
C[0] = xor( D(C[1]) , P[1] )
IV   = xor( D(C[0]) , P[0] )
print(IV) # b'{W3ak_IV_5uckS5}'

And we get the flag: DSC{W3ak_IV_5uckS5}.

Additional ressources

This post is licensed under CC BY 4.0 by the author.