Multipage Recyclings
3 minutes to read
We are provided with the server source code in Python:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import random, os
FLAG = b'HTB{??????????????????????}'
class CAES:
def __init__(self):
self.key = os.urandom(16)
self.cipher = AES.new(self.key, AES.MODE_ECB)
def blockify(self, message, size):
return [message[i:i + size] for i in range(0, len(message), size)]
def xor(self, a, b):
return b''.join([bytes([_a ^ _b]) for _a, _b in zip(a, b)])
def encrypt(self, message):
iv = os.urandom(16)
ciphertext = b''
plaintext = iv
blocks = self.blockify(message, 16)
for block in blocks:
ct = self.cipher.encrypt(plaintext)
encrypted_block = self.xor(block, ct)
ciphertext += encrypted_block
plaintext = encrypted_block
return ciphertext
def leak(self, blocks):
r = random.randint(0, len(blocks) - 2)
leak = [self.cipher.encrypt(blocks[i]).hex() for i in [r, r + 1]]
return r, leak
def main():
aes = CAES()
message = pad(FLAG * 4, 16)
ciphertext = aes.encrypt(message)
ciphertext_blocks = aes.blockify(ciphertext, 16)
r, leak = aes.leak(ciphertext_blocks)
with open('output.txt', 'w') as f:
f.write(f'ct = {ciphertext.hex()}\nr = {r}\nphrases = {leak}\n')
if __name__ == "__main__":
main()
And we also have the output of the script in output.txt
:
ct = bc9bc77a809b7f618522d36ef7765e1cad359eef39f0eaa5dc5d85f3ab249e788c9bc36e11d72eee281d1a645027bd96a363c0e24efc6b5caa552b2df4979a5ad41e405576d415a5272ba730e27c593eb2c725031a52b7aa92df4c4e26f116c631630b5d23f11775804a688e5e4d5624
r = 3
phrases = ['8b6973611d8b62941043f85cd1483244', 'cf8f71416111f1e8cdee791151c222ad']
Source code analysis
The server uses a custom cipher named CAES
based on AES in ECB mode:
Let’s take a look at the encrypt
method:
def encrypt(self, message):
iv = os.urandom(16)
ciphertext = b''
plaintext = iv
blocks = self.blockify(message, 16)
for block in blocks:
ct = self.cipher.encrypt(plaintext)
encrypted_block = self.xor(block, ct)
ciphertext += encrypted_block
plaintext = encrypted_block
return ciphertext
Given some plaintext blocks $p_i$, we have this process to get ciphertexts $c_i$:
- $c_0 = \mathrm{AES}(\mathrm{IV}) \oplus p_0$
- $c_1 = \mathrm{AES}(c_0) \oplus p_1$
- $c_2 = \mathrm{AES}(c_1) \oplus p_2$
- $c_3 = \mathrm{AES}(c_2) \oplus p_3$
- $c_4 = \mathrm{AES}(c_3) \oplus p_4$
- $c_5 = \mathrm{AES}(c_4) \oplus p_5$
- …
Leakage
To make the cipher vulnerable, we are given the output of leak
:
def leak(self, blocks):
r = random.randint(0, len(blocks) - 2)
leak = [self.cipher.encrypt(blocks[i]).hex() for i in [r, r + 1]]
return r, leak
In brief, we are given two consecutive ciphertexts. As shown in output.txt
, we are given $\mathrm{AES}(c_3)$ and $\mathrm{AES}(c_4)$. These leaks can be used to undo the encryption because of XOR properties:
$$ p_4 = c_4 \oplus \mathrm{AES}(c_3) $$
$$ p_5 = c_5 \oplus \mathrm{AES}(c_4) $$
These two operations are enough to solve the challenge because the plaintext is the flag repeated four times:
def main():
aes = CAES()
message = pad(FLAG * 4, 16)
ciphertext = aes.encrypt(message)
ciphertext_blocks = aes.blockify(ciphertext, 16)
r, leak = aes.leak(ciphertext_blocks)
with open('output.txt', 'w') as f:
f.write(f'ct = {ciphertext.hex()}\nr = {r}\nphrases = {leak}\n')
Solution
We can solve the challenge using Python:
$ python3 -q
>>> from pwn import unhex, xor
>>>
>>> def blockify(message, size):
... return [message[i : i + size] for i in range(0, len(message), size)]
...
>>> ct = blockify(unhex('bc9bc77a809b7f618522d36ef7765e1cad359eef39f0eaa5dc5d85f3ab249e788c9bc36e11d72eee281d1a645027bd96a363c0e24efc6b5caa552b2df4979a5ad41e405576d415a5272ba730e27c593eb2c725031a52b7aa92df4c4e26f116c631630b5d23f11775804a688e5e4d5624'), 16)
>>> aes_c3 = unhex('8b6973611d8b62941043f85cd1483244')
>>> aes_c4 = unhex('cf8f71416111f1e8cdee791151c222ad')
>>>
>>> p4 = xor(ct[4], aes_c3)
>>> p5 = xor(ct[5], aes_c4)
>>> p4 + p5
b'_w34k_w17h_l34kz}HTB{CFB_15_w34k'
Flag
Therefore, the flag is: HTB{CFB_15_w34k_w17h_l34kz}
.