AESWCM
5 minutos de lectura
Se nos proporciona el código fuente del servidor en Python:
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
import os
import random
from secret import FLAG
KEY = os.urandom(16)
IV = os.urandom(16)
class AESWCM:
def __init__(self, key):
self.key = key
self.cipher = AES.new(self.key, AES.MODE_ECB)
self.BLOCK_SIZE = 16
def pad(self, pt):
if len(pt) % self.BLOCK_SIZE != 0:
pt = pad(pt, self.BLOCK_SIZE)
return pt
def blockify(self, message):
return [
message[i:i + self.BLOCK_SIZE]
for i in range(0, len(message), self.BLOCK_SIZE)
]
def xor(self, a, b):
return bytes([aa ^ bb for aa, bb in zip(a, b)])
def encrypt(self, pt, iv):
blocks = self.blockify(pt)
xor_block = iv
ct = []
for block in blocks:
ct_block = self.cipher.encrypt(self.xor(block, xor_block))
xor_block = self.xor(block, ct_block)
ct.append(ct_block)
return b"".join(ct).hex()
def decrypt(self, ct, iv):
ct = bytes.fromhex(ct)
blocks = self.blockify(ct)
xor_block = iv
pt = []
for block in blocks:
pt_block = self.xor(self.cipher.decrypt(block), xor_block)
xor_block = self.xor(block, pt_block)
pt.append(pt_block)
return b"".join(pt)
def tag(self, pt, iv=os.urandom(16)):
blocks = self.blockify(bytes.fromhex(self.encrypt(pt, iv)))
random.shuffle(blocks)
ct = blocks[0]
for i in range(1, len(blocks)):
ct = self.xor(blocks[i], ct)
return ct.hex()
def main():
aes = AESWCM(KEY)
tags = []
properties = []
print("What properties should your magic wand have?")
message = "Property: "
counter = 0
while counter < 3:
property = bytes.fromhex(input(message))
property = aes.pad(message.encode() + property)
if property not in properties:
properties.append(property)
property_tag = aes.tag(property, IV)
tags.append(property_tag)
print(property_tag)
if len(tags) > len(set(tags)):
print(FLAG)
counter += 1
else:
print("Only different properties are allowed!")
exit(1)
if __name__ == "__main__":
main()
Análisis del código fuente
En primer lugar, el programa inicializa las variables KEY
e IV
a 16 bytes aleatorios. Después de eso, se crea una instancia de clase AESWCM
.
Luego, se nos pide que ingresemos un mensaje que se etiquetará. Nuestro mensaje (property
) se rellenará con aes.pad
y se agregará a la lista de mensajes (properties
). Y la etiqueta (property_tag
) se agregará a la lista de etiquetas (tags
).
Veremos la flag cuando len(tags) > len(set(tags))
; es decir, cuando haya un elemento repetido en tags
(porque no hay elementos duplicados en un set
).
El siguiente código refleja la explicación anterior:
message = "Property: "
counter = 0
while counter < 3:
property = bytes.fromhex(input(message))
property = aes.pad(message.encode() + property)
if property not in properties:
properties.append(property)
property_tag = aes.tag(property, IV)
tags.append(property_tag)
print(property_tag)
if len(tags) > len(set(tags)):
print(FLAG)
counter += 1
else:
print("Only different properties are allowed!")
exit(1)
Análisis del algoritmo de cifrado
La forma de etiquetar un mensaje (property
) es llamando al método tag
(es decir, "Property: "
concatenado con nuestros datos de entrada con relleno).
Este es el método tag
:
def tag(self, pt, iv=os.urandom(16)):
blocks = self.blockify(bytes.fromhex(self.encrypt(pt, iv)))
random.shuffle(blocks)
ct = blocks[0]
for i in range(1, len(blocks)):
ct = self.xor(blocks[i], ct)
return ct.hex()
Lo que hace es: encripta el texto plano usando el método encrypt
, divide el resultado en bloques de 16 bytes usando blockify
y finalmente, baraja los bloques y aplica XOR a todos ellos.
Aquí podemos ver que el shuffle es inútil, porque la operación XOR es conmutativa, el orden no importa.
Uso de AES y XOR
Este es el método encrypt
:
def encrypt(self, pt, iv):
blocks = self.blockify(pt)
xor_block = iv
ct = []
for block in blocks:
ct_block = self.cipher.encrypt(self.xor(block, xor_block))
xor_block = self.xor(block, ct_block)
ct.append(ct_block)
return b"".join(ct).hex()
Básicamente, divide el mensaje en bloques de 16 bytes y realiza el cifrado. Recordemos que el cifrado es AES ECB:
class AESWCM:
def __init__(self, key):
self.key = key
self.cipher = AES.new(self.key, AES.MODE_ECB)
self.BLOCK_SIZE = 16
Sin embargo, la función encrypt
es bastante similar al modo AES CBC, pero no exactamente igual. A continuación, puede ver cómo es el cifrado AES CBC:
Esta vez, las realimentaciones de cada bloque al siguiente no son los bloques de texto cifrado, sino un XOR entre los bloques de texto cifrado con los bloques de mensaje. Digamos que tenemos 3 bloques de mensaje (
Solución
Para la solución, evitaremos el relleno ingresando mensajes usando un tamaño múltiplo de 16 bytes. Sea
Para la primera ronda pondremos un solo bloque, y tendremos
.
Entonces estableceremos:
De forma que
.
Obsérvese que
Entonces podemos simplificar
Finalmente, pondremos
Y entonces tendremos que
Nótese que
Y por otro lado,
Por lo tanto,
Como
Implementación en Python
def main():
io = get_process()
p_1_1 = b'Property: ' + bytes.fromhex('00' * 6)
io.sendlineafter(b'Property: ', p_1_1[10:].hex().encode())
e_1 = c_1_1 = bytes.fromhex(io.recvline().decode())
p_2_1 = p_1_1
p_2_2 = xor(e_1, p_1_1)
io.sendlineafter(b'Property: ', (p_2_1 + p_2_2)[10:].hex().encode())
e_2 = bytes.fromhex(io.recvline().decode())
c_2_1 = c_1_1
c_2_2 = xor(e_2, c_2_1)
p_3_1 = p_2_1
p_3_2 = p_2_2
p_3_3 = xor(p_2_2, c_2_2)
io.sendlineafter(b'Property: ', (p_3_1 + p_3_2 + p_3_3)[10:].hex().encode())
io.recvline()
log.success(f'Flag: {io.recvline()}')
io.close()
$ python3 solve.py 178.62.21.211:32535
[+] Opening connection to 178.62.21.211 on port 32535: Done
[+] Flag: b'HTB{435_cu570m_m0d35_4nd_hm4c_423_fun}'
[*] Closed connection to 178.62.21.211 port 32535
El script completo se puede encontrar aquí: solve.py
.