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 = b25bc89662197c6462188e5960eea4fbef11424b8ebdcd6b45c8f4240d64f5d1981aab0e299ff75ce9fba3d5d78926543e5e8c262b81090aef60518ee241ab131db902d2582a36618f3b9a85a35f52352d5499861b4a878fac1380f520fe13deb1ca50c64f30e98fa6fdc070d02e148f
r = 3
phrases = ['5fe633e7071e690fbe58a9dace6f3606', '501ccdc4600bc2dcf350c6b77fcf2681']
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('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
Por lo tanto, la flag es: HTB{AES_CFB_15_4_n1c3_m0d3}
.