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 ($p_1$, $p_2$, $p_3$):
$$ c_1 = \mathrm{AES}\big(p_1 \oplus \mathrm{IV}\big) $$ $$ c_2 = \mathrm{AES}\big(p_2 \oplus (p_1 \oplus c_1)\big) $$ $$ c_3 = \mathrm{AES}\big(p_3 \oplus (p_2 \oplus c_2)\big) $$
Solución
Para la solución, evitaremos el relleno ingresando mensajes usando un tamaño múltiplo de 16 bytes. Sea $p_{i,j}$ el mensaje que enviamos al servidor, $c_{i,j}$ la salida del cifrado AES y $e_i$ el resultado de la operación XOR entre los bloques, donde $i$ representa el número re ronda y $j$ es el índice del bloque.
Para la primera ronda pondremos un solo bloque, y tendremos $e_1 = c_{1,1}$, donde
- $c_{1,1} = \mathrm{AES}(p_{1,1} \oplus \mathrm{IV})$.
Entonces estableceremos:
- $p_{2,1} = p_{1,1}$
- $p_{2,2} = e_1 \oplus p_{1,1}$
De forma que $e_2 = c_{2,1} \oplus c_{2,2}$, donde
- $c_{2,1} = \mathrm{AES}(p_{2,1} \oplus \mathrm{IV})$
- $c_{2,2} = \mathrm{AES}\big(p_{2,2} \oplus (p_{2,1} \oplus c_{2,1})\big)$.
Obsérvese que $c_{2,1} = c_{1,1} = e_1$ y
$$ c_{2,2} = \mathrm{AES}\big((e_1 \oplus p_{1,1}) \oplus (p_{1,1} \oplus e_1)\big) = \mathrm{AES}(0) $$
Entonces podemos simplificar $e_2 = e_1 \oplus \mathrm{AES}(0)$.
Finalmente, pondremos
- $p_{3,1} = p_{2,1}$
- $p_{3,2} = p_{2,2}$
- $p_{3,3} = p_{2,2} \oplus c_{2,2}$
Y entonces tendremos que $e_3 = c_{3,1} \oplus c_{3,2} \oplus c_{3,3}$, donde
- $c_{3,1} = \mathrm{AES}(p_{3,1} \oplus \mathrm{IV})$
- $c_{3,2} = \mathrm{AES}\big(p_{3,2} \oplus (p_{3,1} \oplus c_{3,1})\big)$
- $c_{3,3} = \mathrm{AES}\big(p_{3,3} \oplus (p_{3,2} \oplus c_{3,2})\big)$
Nótese que
- $c_{3,1} = c_{2,1} = c_{1,1} = e_1$
- $c_{3,2} = c_{2,2} = \mathrm{AES}(0)$
Y por otro lado,
$$ c_{3,3} = \mathrm{AES}\big((p_{2,2} \oplus c_{2,2}) \oplus (p_{3,2} \oplus c_{3,2})\big) = \mathrm{AES}(0) $$
Por lo tanto, $e_3 = c_{3,1} \oplus c_{3,2} \oplus c_{3,3} = e_1 \oplus \mathrm{AES}(0) \oplus \mathrm{AES}(0) = e_1$.
Como $e_3 = e_1$, la lista de etiquetas tendrá un elemento duplicado y obtendremos la flag.
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
.