Initialization
4 minutes to read
We are given the Python source code to encrypt the flag:
#!/usr/bin/env python3
import os
from Crypto.Util import Counter
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
class AdvancedEncryption:
def __init__(self, block_size):
self.KEYS = self.generate_encryption_keys()
self.CTRs = [Counter.new(block_size) for i in range(len(MSG))] # nonce reuse : avoided!
def generate_encryption_keys(self):
keys = [[b'\x00']*16] * len(MSG)
for i in range(len(keys)):
for j in range(len(keys[i])):
keys[i][j] = os.urandom(1)
return keys
def encrypt(self, i, msg):
key = b''.join(self.KEYS[i])
ctr = self.CTRs[i]
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
return cipher.encrypt(pad(msg.encode(), 16))
def main():
AE = AdvancedEncryption(128)
with open('output.txt', 'w') as f:
for i in range(len(MSG)):
ct = AE.encrypt(i, MSG[i])
f.write(ct.hex()+'\n')
if __name__ == '__main__':
with open('messages.txt') as f:
MSG = eval(f.read())
main()
Source code analysis
First of all, the script takes some messages from messages.txt
and stores them in MSG
. Then it runs main
:
if __name__ == '__main__':
with open('messages.txt') as f:
MSG = eval(f.read())
main()
In main
, the script defines an instance of AdvancedEncryption
, encrypts each message in MSG
and outputs the results into output.txt
:
def main():
AE = AdvancedEncryption(128)
with open('output.txt', 'w') as f:
for i in range(len(MSG)):
ct = AE.encrypt(i, MSG[i])
f.write(ct.hex()+'\n')
We are also given messages.txt
:
[
'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.',
]
And output.txt
:
2ac199d1395745812e3e5d3c4dc995cd2f2a076426b70fd5209cdd5ddc0a0c372feb3909956a791702180f591a63af184c27a6ba2fd61c1741ea0818142d0b92
30c6d0cd775b16c23c3f103a1fd883c4632c11366fbc07d92088cc5ddc0a0c373aef3f12c7606c114f546c7f6e00c87a
36fdb2d97d0a5bcf0225586a1e8abfc62d3057273aab5ae5309d8c4ade060a236aed070d817b2c14110e590b1b27ef5d4d35ddc001b47d6c2bca00101c25039a
2dcc93d07c4a16c833375f2b00d894c62c2d442d3cf90cd43183c559c10006372cea2c1595487c0f4314091c0c268b120f3aaabe7bd31c0c05977a7f7c4f6ce6f59392e0e522e66500e153f7a6f914c7
The encryption method implemented in AdvancedEncryption
is just AES in CTR mode:
class AdvancedEncryption:
def __init__(self, block_size):
self.KEYS = self.generate_encryption_keys()
self.CTRs = [Counter.new(block_size) for i in range(len(MSG))] # nonce reuse : avoided!
def generate_encryption_keys(self):
keys = [[b'\x00']*16] * len(MSG)
for i in range(len(keys)):
for j in range(len(keys[i])):
keys[i][j] = os.urandom(1)
return keys
def encrypt(self, i, msg):
key = b''.join(self.KEYS[i])
ctr = self.CTRs[i]
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
return cipher.encrypt(pad(msg.encode(), 16))
This mode is just a stream cipher, so the encryption algorithm is just XOR the plaintext with a cipher stream that comes from AES CTR:
As a result, if we have a pair of known plaintext-ciphertext, and the same cipher stream is reused for another ciphertext, we are able to find the cipher stream and decrypt the rest.
The security flaw
It might seem that each message uses a different AES key, but there is an error:
def generate_encryption_keys(self):
keys = [[b'\x00']*16] * len(MSG)
for i in range(len(keys)):
for j in range(len(keys[i])):
keys[i][j] = os.urandom(1)
return keys
All cipher streams will be generated by the same key because [[...]*16] * len(MSG)
only generates 4 references to the same key (because of how Python works). We can test it easily:
$ python3 -q
>>> import os
>>>
>>> keys = [[b'\x00']*16] * 4
>>> keys
[[b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00'], [b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00'], [b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00'], [b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00']]
>>>
>>> for i in range(len(keys)):
... for j in range(len(keys[i])):
... keys[i][j] = os.urandom(1)
...
>>> keys
[[b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0'], [b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0'], [b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0'], [b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0']]
>>>
>>> keys[0]
[b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0']
>>> keys[1]
[b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0']
>>> keys[2]
[b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0']
>>> keys[3]
[b'\x06', b'e', b'6', b'\xd8', b'l', b'\xa4', b'\xaa', b'[', b'\xbd', b'\xad', b'\xa0', b'w', b'\xc3', b'\xac', b'\x8d', b'\xa0']
As can be seen, the 4 keys are the same, so we know that:
$$ C_i = K \oplus P_i \qquad\forall i \in \{0,1,2,3\} $$
Where $C_i$ are the ciphertexts, $P_i$ are the plaintext messages and $K$ is the cipher stream (the same for each plaintext-ciphertext pair). Therefore, from one pair, we have
$$ K = C_3 \oplus P_3 $$
And we are interested in $P_2$, so
$$ P_2 = C_2 \oplus K = C_2 \oplus C_3 \oplus P_3 $$
Flag
We can obtain the flag easily with Python:
>>> from pwn import unhex, xor
>>>
>>> P = [
... '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.',
... ]
>>> C = '''2ac199d1395745812e3e5d3c4dc995cd2f2a076426b70fd5209cdd5ddc0a0c372feb3909956a791702180f591a63af184c27a6ba2fd61c1741ea0818142d0b92
... 30c6d0cd775b16c23c3f103a1fd883c4632c11366fbc07d92088cc5ddc0a0c373aef3f12c7606c114f546c7f6e00c87a
... 36fdb2d97d0a5bcf0225586a1e8abfc62d3057273aab5ae5309d8c4ade060a236aed070d817b2c14110e590b1b27ef5d4d35ddc001b47d6c2bca00101c25039a
... 2dcc93d07c4a16c833375f2b00d894c62c2d442d3cf90cd43183c559c10006372cea2c1595487c0f4314091c0c268b120f3aaabe7bd31c0c05977a7f7c4f6ce6f59392e0e522e66500e153f7a6f914c7
... '''.splitlines()
>>>
>>> xor(unhex(C[2]), unhex(C[3]), P[3].encode())
b'HTB{d4mn_th3s3_ins3cur3_bl0ckch41n_p4r4m3t3rs!!!!}\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\x0e\xa7\x1d\x0ej\xfdK\xcf\xcfv\xe4b\xf3\xde\x1c\xd9l'
>>>