Multipage Recyclings
3 minutos de lectura
Se nos proporciona el código fuente del servidor en 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()
Y también tenemos la salida del script en output.txt
:
ct = bc9bc77a809b7f618522d36ef7765e1cad359eef39f0eaa5dc5d85f3ab249e788c9bc36e11d72eee281d1a645027bd96a363c0e24efc6b5caa552b2df4979a5ad41e405576d415a5272ba730e27c593eb2c725031a52b7aa92df4c4e26f116c631630b5d23f11775804a688e5e4d5624
r = 3
phrases = ['8b6973611d8b62941043f85cd1483244', 'cf8f71416111f1e8cdee791151c222ad']
Análisis del código fuente
El servidor usa un cifrado personalizado con nombre CAES
basado en AES en modo ECB:
Echemos un vistazo al método encrypt
:
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
Dados algunos bloques de texto plano $p_i$, tenemos este proceso para obtener texto cifrado $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$
- …
Filtración
Para que el cifrado sea vulnerable, se nos da la salida de 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
En resumen, se nos dan dos textos cifrados consecutivos. Como se muestra en output.txt
, tenemos $\mathrm{AES}(c_3)$ y $\mathrm{AES}(c_4)$. Estas filtraciones se pueden usar para deshacer el cifrado debido a las propiedades de XOR:
$$ p_4 = c_4 \oplus \mathrm{AES}(c_3) $$
$$ p_5 = c_5 \oplus \mathrm{AES}(c_4) $$
Estas dos operaciones son suficientes para resolver el reto porque el texto plano es la flag repetida cuatro veces:
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')
Solución
Podemos resolver el reto usando 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
Por lo tanto, la flag es: HTB{CFB_15_w34k_w17h_l34kz}
.