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 = b25bc89662197c6462188e5960eea4fbef11424b8ebdcd6b45c8f4240d64f5d1981aab0e299ff75ce9fba3d5d78926543e5e8c262b81090aef60518ee241ab131db902d2582a36618f3b9a85a35f52352d5499861b4a878fac1380f520fe13deb1ca50c64f30e98fa6fdc070d02e148f
r = 3
phrases = ['5fe633e7071e690fbe58a9dace6f3606', '501ccdc4600bc2dcf350c6b77fcf2681']
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('b25bc89662197c6462188e5960eea4fbef11424b8ebdcd6b45c8f4240d64f5d1981aab0e299ff75ce9fba3d5d78926543e5e8c262b81090aef60518ee241ab131db902d2582a36618f3b9a85a35f52352d5499861b4a878fac1380f520fe13deb1ca50c64f30e98fa6fdc070d02e148f'), 16)
>>> aes_c3 = unhex('5fe633e7071e690fbe58a9dace6f3606')
>>> aes_c4 = unhex('501ccdc4600bc2dcf350c6b77fcf2681')
>>>
>>> p4 = xor(ct[4], aes_c3)
>>> p5 = xor(ct[5], aes_c4)
>>> p4 + p5
b'B_15_4_n1c3_m0d3}HTB{AES_CFB_15_'
Flag
Therefore, the flag is: HTB{AES_CFB_15_4_n1c3_m0d3}
.