RITSEC CTF writeup - recover AES-CBC IV

RITSEC CTF writeup - recover AES-CBC IV

This is a writeup for the crypto challenge "Who draw on my program?" from RITSEC CTF 2018.

The flag is the initialisation vector - © sandw1ch RITSEC



If you have no clue what a ctf is, have a look here

What is AES?

AES (Advanced Encryption Standard), also known as Rijndael cipher, is a block cipher for symmetric cryptography.
For this challenge we do not need to understand how AES itself works, there happens a lot of math magic with substitutions and permutations. If you are interested in the details, have a look at wikipedia.

What is CBC, what is an IV?

Cipher Block Chaining (CBC) is a mode of operation for AES.
The plaintext and ciphertext are divide into blocks of a defined length (remember: AES is a block cipher). Before encrypting the plaintext block number n with AES, it becomes xored with the ciphertext block n-1. Because the first plaintext block has no cipher text befor itself to xor with, there is an initialisation vector (IV) for this (it is also used for randomizing the input, but this is not relevant here).



What do we know?

  • The encryption algorithm (AES CBC with block size 16)
  • The plaintext ("The message is protected by AES!")
  • first 14 characters of the 16 character key
  • The complete second block and parts of the first ciphertext block

Let's have a look on decryption with CBC:


Because we have only 2 blocks here, we can ignore the third block at the diagram.
Please note: The IV is only needed for decrypting the first block, not for the second one!

Get the AES key

We know the second ciphertext block, the plaintext and parts of the first block of ciphertext. Therefor we can brute force the key's last two characters by decrypting the second block of ciphertext with all possible keys, xoring with the first block of ciphertext (the unknown parts padded by zeros) and see, for which key the first letter and last two letters of the result match to the original plaintext.

from Crypto.Cipher import AES
from operator import xor
import binascii, sys

KEY_first = "9aF738g9AkI112"
cipher1 = "9e00000000000000000000000000436a" 
cipher2 = "808e200a54806b0e94fb9633db9d67f0"
plain1 = "The message is p"
plain2 = "rotected by AES!"

def decrypt(cipher, passphrase):
    aes = AES.new(passphrase, AES.MODE_CBC, binascii.unhexlify(cipher1))
    return aes.decrypt(cipher)

# iterate through relavent ascii range
for i in range(32, 126):
    for j in range(32, 126):
        key = KEY_first + chr(i) + chr(j)
        dec_plain2 = decrypt(binascii.unhexlify(cipher2),  key)
        if  str(dec_plain2).startswith("r") and str(dec_plain2).endswith('S!'):
            print "decrypted plain2: " + dec_plain2 + " with key: " + key

decrypted plain2: r}�������G�2�S! with key: 9aF738g9AkI112#g
The not printable characters emerge by xoring with the unknown and therefor zero-padded part of the first ciphertext block.

Get the first ciphertext block

Remember: After decrypting the block number n it will be xored with block number n-1 to produce the plaintext.
Scince xor is revertable \( a \oplus b = c \Leftrightarrow c \oplus b = a \) and we now know the key, we can simply change the role of the first cipher text block and second plaintext block and do AES decryption on the second ciphertext block with the second plaintext block as IV (instead of the first cipher text block), which leads to the first ciphertext block as "decrypted paintext".

from Crypto.Cipher import AES
import binascii, sys

KEY = "9aF738g9AkI112#g"
plain2 = "rotected by AES!"

cipher2= "808e200a54806b0e94fb9633db9d67f0"

def decrypt(cipher,passphrase):
    aes = AES.new(passphrase,AES.MODE_CBC,plain2)
    return aes.decrypt(cipher)

# Output result
print "Decrypted data: " + binascii.hexlify(decrypt(binascii.unhexlify(cipher2), KEY))

Output: decrypted data: 9e128e7bc9ab9cc9d8b13ec77389436a

Get the IV

We can do the trick with changing roles again:
This time we change the role of first plaintext block and IV when doing decryption on the first ciphertext block, which leads to the IV as decrypted plaintext.

from Crypto.Cipher import AES
import binascii, sys

IV="The message is p"


def decrypt(cipher,passphrase):
    aes = AES.new(passphrase,AES.MODE_CBC,IV)
    return aes.decrypt(cipher)

print "decrypted data: " + decrypt(binascii.unhexlify(cipher1), KEY)

Output: decrypted data: RITSEC{b4dcbc#g}

We got the flag! \o/