← Back to blog

Initialization

Files

Download: crypto_initialization.zip

Challenge

During a cyber security audit of your government’s infrastructure, you discover log entries showing traffic directed towards an IP address within the enemy territory of “Oumara”. This alarming revelation triggers suspicion of a mole within Lusons’ government. Determined to unveil the truth, you analyze the encryption scheme with the goal of breaking it and decrypting the suspicious communication. Your objective is to extract vital information and gather intelligence, ultimately protecting your nation from potential threats.

Recon

The challenge centers on AES-128 in CBC mode with a reused key and a known plaintext. The fundamental weakness here is keystream/key reuse combined with knowing one of the plaintexts, which lets us recover the others without ever learning the key.

This YouTube video gives a good explanation of the underlying technique:

https://www.youtube.com/watch?v=Gtfr1dBGzHg

To understand the program’s behaviour, I first ran it after adding a print(self.KEYS) statement. The output confirmed that the same key is reused for every encryption. Because the key (and nonce) are reused across messages, the keystream is identical for each ciphertext.

Analysis

When the same key/keystream encrypts multiple messages, XORing two ciphertexts cancels out the key entirely, leaving the XOR of the two plaintexts. Since one plaintext is known, we can recover the unknown flag with:

flag = (ciphertext1 ⊕ ciphertext2) ⊕ known_plaintext

The challenge provides four messages, one of which is the flag, along with their corresponding ciphertexts.

Exploitation

The following script XORs the first ciphertext (whose plaintext we know) with the third ciphertext (the flag), then XORs in the known plaintext. The key cancels because it is shared, exposing the flag bytes.

from binascii import unhexlify
from pwn import xor
MSG = [
        'This is some public information that can be read out loud.',
        'No one can crack our encryption algorithm.',
        'HTB{?????????????????????????????????????????????}',
        'Secret information is encrypted with Advanced Encryption Standards.'
]
CIPHERTEXTS = [
    "76ca21043b5e471169ec20a55297165807ab5b30e588c9c54168b2136fc97d147892b5e39e9b1f1fd39e9f66e7dbbb9d8dffa31b597b53a648676a8d4081a20b",
    "6ccd6818755214527bed6da3008600514bad4d62ac83c1c9417ca3136fc97d146d96b3f8cc910a199ed2fc4093b8dcff",
    "6af60a0c6e5944432af77ea30682076509ae0873e785c79e026b8c1435c566463d8eadc8cecc0c459ecf8e75e7cdfbd88cedd861771932dd224762854889aa03",
    "71c72b057e43145874e522b21f86175304ac1879ffc6cac45077aa1772c377147b93a0ff9eb91a0792929923f19e9f97cee2af1f0d7e53bd0c1a18ea28e3c57fd718b40f5d2c0014a3dbe6a3e5654fe8"
]
flag = xor(unhexlify(CIPHERTEXTS[0]), unhexlify(CIPHERTEXTS[2]), MSG[0])
print(flag)

Running it prints the flag (with some trailing garbage past the end of the shorter known plaintext):

b'HTB{unpr0t3cted_bl0ckch41n_s3cur1ty_p4r4m3t3rs!!!}/\x12G\x11A\x12\x19\x00{gem(x'

The same operation can be performed visually in CyberChef using From Hex followed by two XOR operations (the known plaintext and the flag ciphertext):

https://gchq.github.io/CyberChef/#recipe=From_Hex(‘Auto’)XOR(%7B’option’:‘UTF8’,‘string’:‘This%20is%20some%20public%20information%20that%20can%20be%20read%20out%20loud.’%7D,‘Standard’,false)XOR(%7B’option’:‘Hex’,‘string’:‘6af60a0c6e5944432af77ea30682076509ae0873e785c79e026b8c1435c566463d8eadc8cecc0c459ecf8e75e7cdfbd88cedd861771932dd224762854889aa03’%7D,‘Standard’,false)&input=NzZjYTIxMDQzYjVlNDcxMTY5ZWMyMGE1NTI5NzE2NTgwN2FiNWIzMGU1ODhjOWM1NDE2OGIyMTM2ZmM5N2QxNDc4OTJiNWUzOWU5YjFmMWZkMzllOWY2NmU3ZGJiYjlkOGRmZmEzMWI1OTdiNTNhNjQ4Njc2YThkNDA4MWEyMGI

To clean up the trailing garbage, this alternative script brute-forces each flag character (over printable ASCII) constrained so that the recovered known plaintext matches exactly, byte by byte:

#!/usr/bin/env python3

from pwn import xor

m1 = b'This is some public information that can be read out loud.'

c1 = bytes.fromhex('76ca21043b5e471169ec20a55297165807ab5b30e588c9c54168b2136fc97d147892b5e39e9b1f1fd39e9f66e7dbbb9d8dffa31b597b53a648676a8d4081a20b')
c3 = bytes.fromhex('6af60a0c6e5944432af77ea30682076509ae0873e785c79e026b8c1435c566463d8eadc8cecc0c459ecf8e75e7cdfbd88cedd861771932dd224762854889aa03')

AxB = xor(c1, c3)

flag = ''
for i in range(len(m1)):
    for j in range(32, 127):
        maybe = AxB[i] ^ j
        if maybe == m1[i]:
            flag += chr(j)

print(flag)

'''
Reusing key/nonce affects security of CTR mode and Stream ciphers in general.
Assume that you have two ciphertexts encrypted with the same key, E(A) and E(B).

E(A) = key xor A
E(B) = key xor B

Now try XORing the two ciphertexts as follows:

E(A) xor E(B) = key xor A XOR key xor B

= A xor key xor key xor B // algebraic property of xor
= A xor 0 xor B           // because key xor key yields 0
= A xor B                 // XORing 0 with anything yields that thing

Given that A and B are normal English letters, the guessing of A and B will be
trivial, as you lost the key space of the stream cipher. Now, you are just trying
26 letters.

The worst case scenario applies when A and B have the same length. Efficiently,
you will break two ciphertexts at one shot.
'''

Flag

HTB{unpr0t3cted_bl0ckch41n_s3cur1ty_p4r4m3t3rs!!!}