Introduction
This post covers a cryptographic HackTheBox Initialization (CTF) challenge that uses Python for encrypting messages with AES in CTR mode. The challenge demonstrates a security flaw caused by repeated key use, allowing cipher stream reuse across messages. This vulnerability permits decryption by XOR-ing ciphertext and known plaintext values. The walkthrough includes Python code to exploit this flaw, recover the cipher stream, and reveal the encrypted flag.
HackTheBox Certified Defensive Security Analyst Study Notes
HackTheBox Certified Penetration Testing Specialist Study Notes
HackTheBox Initialization Description
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.
Source Code Analysis
Initially, the script retrieves messages from messages.txt
and saves them in MSG
. Afterward, it executes main
Within main
, the script creates an instance of AdvancedEncryption
, encrypts each message in MSG
, and writes the encrypted results to output.txt
.
The encryption method used in AdvancedEncryption
is simply AES in CTR mode.
Since this mode operates as a stream cipher, the encryption algorithm simply XORs the plaintext with a cipher stream generated by AES in CTR mode.
#!/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()
Consequently, if we have a known plaintext-ciphertext pair and the same cipher stream is reused for another ciphertext, we can determine the cipher stream and decrypt the remaining ciphertexts.
What is AES in CTR Mode?
AES (Advanced Encryption Standard) cryptography in CTR (Counter) mode is a method of encrypting data that combines the security of AES with a stream cipher-like functionality, making it highly suitable for encrypting large data streams and enabling parallel processing. Here’s a breakdown of how AES in CTR mode works and what makes it unique:
1. AES Cryptography Overview
- AES is a symmetric encryption algorithm widely used for securing sensitive data.
- It operates on fixed block sizes (typically 128 bits) and uses key sizes of 128, 192, or 256 bits.
- AES encrypts blocks of data, which means data larger than a single block must be split into multiple blocks before encryption.
2. What is CTR Mode?
- CTR (Counter) Mode is a mode of operation for block ciphers like AES, designed to convert AES into a stream cipher by using a unique “counter” value.
- CTR mode generates a “keystream” that is XORed with plaintext to produce ciphertext or with ciphertext to recover plaintext.
- This approach makes CTR mode very different from traditional block cipher modes like ECB (Electronic Codebook) or CBC (Cipher Block Chaining).
3. How AES in CTR Mode Works
- Counter Initialization: In CTR mode, a nonce (a unique starting value) and a counter (incremented with each block) are combined to create a unique input for each encryption operation.
- Encryption Process:
- The nonce and counter are encrypted using AES to produce a block of keystream.
- The resulting keystream block is XORed with the plaintext block to produce the ciphertext.
- For the next block, the counter is incremented, and the process repeats.
- Decryption Process: Decryption is essentially the same as encryption since the keystream generation is reversible. The keystream is XORed with the ciphertext to recover the plaintext.
Vulnerability Analysis
All cipher streams will be generated using the same key because [[...]*16] * len(MSG)
in Python only creates four references to the same key, rather than unique keys. This can be easily tested.
$ 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']
Therefore the flag can then be obtained:
>>> 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'
>>>
You can also watch: