Extraño problema con el modo AES CTR con Python y Javascript

Estoy tratando de descifrar un texto cifrado creado por CryptoJS usando PyCrypto. Estoy usando AES-256-CTR, con un prefijo aleatorio de 12 bytes y un contador de 4 bytes. Hasta ahora, he tenido un éxito limitado. Por favor, lea este post anterior donde hice un primer bash.

Esto funciona en Javascript:

  1. Instale la extensión CryptoCat
  2. Ejecutar CryptoCat
  3. Encienda la consola de desarrollador (F12 en Chrome / Firefox)
  4. Ejecuta estas lineas de codigo

key = 'b1df40bc2e4a1d4e31c50574735e1c909aa3c8fda58eca09bf2681ce4d117e11'; msg = 'LwFUZbKzuarvPR6pmXM2AiYVD2iL0/Ww2gs/9OpcMy+MWasvvzA2UEmRM8dq4loB\ndfPaYOe65JqGQMWoLOTWo1TreBd9vmPUZt72nFs='; iv = 'gpG388l8rT02vBH4'; opts = {mode: CryptoJS.mode.CTR, iv: CryptoJS.enc.Base64.parse(iv), padding: CryptoJS.pad.NoPadding}; CryptoJS.AES.decrypt(msg, CryptoJS.enc.Hex.parse(key), opts).toString(CryptoJS.enc.Utf8); 

Salida esperada: "Hello, world!ImiAq7aVLlmZDM9RfhDQgPp0CrAyZE0lyzJ6HDq4VoUmIiKUg7i2xpTSPs28USU8" .


Aquí hay un script que escribí en Python que descifra parcialmente el texto cifrado (!):

 import struct import base64 import Crypto.Cipher.AES import Crypto.Util.Counter def bytestring_to_int(s): r = 0 for b in s: r = r * 256 + ord(b) return r class IVCounter(object): def __init__(self, prefix="", start_val=0): self.prefix = prefix self.first = True self.current_val = start_val def __call__(self): if self.first: self.first = False else: self.current_val += 1 postfix = struct.pack(">I", self.current_val) n = base64.b64decode(self.prefix) + postfix return n def decrypt_msg(key, msg, iv): k = base64.b16decode(key.upper()) ctr = IVCounter(prefix=iv) #ctr = Crypto.Util.Counter.new(32, prefix=base64.b64decode(iv), initial_value=0, little_endian=False) aes = Crypto.Cipher.AES.new(k, mode=Crypto.Cipher.AES.MODE_CTR, counter=ctr) plaintext = aes.decrypt(base64.b64decode(msg)) return plaintext if __name__ == "__main__": #original: key = 'b1df40bc2e4a1d4e31c50574735e1c909aa3c8fda58eca09bf2681ce4d117e11' msg = 'LwFUZbKzuarvPR6pmXM2AiYVD2iL0/Ww2gs/9OpcMy+MWasvvzA2UEmRM8dq4loB\ndfPaYOe65JqGQMWoLOTWo1TreBd9vmPUZt72nFs=' iv = 'gpG388l8rT02vBH4' decrypted = decrypt_msg(key, msg, iv) print "Decrypted message:", repr(decrypt_msg(key, msg, iv)) print decrypted 

La salida es:

'Hello, world!Imi\xfb+\xf47\x04\xa0\xb1\xa1\xea\xc0I\x03\xec\xc7\x13d\xcf\xe25>l\xdc\xbd\x9f\xa2\x98\x9f$\x13a\xbb\xcb\x13 \ xd2 # \ xc9T \ xf4 | \ xd8 \ xcb aO)\x94\x9aq<\xa7\x7f\x14\x11\xb5\xb0\xb6\xb5GQ\x92'

El problema es que solo los primeros 16 bytes de la salida coinciden con los primeros 16 bytes de la salida esperada.

Hello, world!ImiAq7aVLlmZDM9RfhDQgPp0CrAyZE0lyzJ6HDq4VoUmIiKUg7i2xpTSPs28USU8

Cuando modifico el script para hacer esto:

 def __init__(self, prefix="", start_val=1): 

y

 self.current_val += 0 #do not increment 

lo que hace que el contador \x00\x00\x00\x01 el mismo valor ( \x00\x00\x00\x01 ) cada vez que se llama, el texto plano es:

 \xf2?\xaf:=\xc0\xfd\xbb\xdf\xf6h^\x9f\xe8\x16I\xfb+\xf47\x04\xa0\xb1\xa1\xea\xc0I\x03\xec\xc7\x13dQgPp0CrAyZE0lyzJ\xa8\xcd!?h\xc9\xa0\x8b\xb6\x8b\xb3_*\x7f\xf6\xe8\x89\xd5\x83H\xf2\xcd'\xc5V\x15\x80k] 

donde el segundo bloque de 16 bytes (dQgPp0CrAyZE0lyzJ) coincide con la salida esperada.

Cuando configuro el contador para que emita \x00\x00\x00\x02 y \x00\x00\x00\x03 , obtengo resultados similares: se revelan bloques de 16 bytes posteriores. La única excepción es que con 0s, se revelan los primeros 32 bytes.

 All 0s: reveals first 32 bytes. 'Hello, world!ImiAq7aVLlmZDM9RfhD\xeb=\x93&b\xaf\xaf\x8d\xc9\xdeA\n\xd2\xd8\x01j\x12\x97\xe2i:%}G\x06\x0f\xb7e\x94\xde\x8d\xc83\x8f@\x1e\xa0!\xfa\t\xe6\x91\x84Q\xe3' All 1s: reveals next 16 bytes. "\xf2?\xaf:=\xc0\xfd\xbb\xdf\xf6h^\x9f\xe8\x16I\xfb+\xf47\x04\xa0\xb1\xa1\xea\xc0I\x03\xec\xc7\x13dQgPp0CrAyZE0lyzJ\xa8\xcd!?h\xc9\xa0\x8b\xb6\x8b\xb3_*\x7f\xf6\xe8\x89\xd5\x83H\xf2\xcd'\xc5V\x15\x80k]" All 2s: reveals next 16 bytes. 'l\xba\xcata_2e\x044\xb2J\xe0\xf0\xd7\xc8e\xae\x91yX?~\x7f1\x02\x93\x17\x93\xdf\xd2\xe5\xcf\xe25>l\xdc\xbd\x9f\xa2\x98\x9f$\x13a\xbb\xcb6HDq4VoUmIiKUg7i\x17P\xe6\x06\xaeR\xe8\x1b\x8d\xd7Z\x7f"' All 3s: reveals next 13 bytes. 'I\x92\\&\x9c]\xa9L\xb1\xb6\xbb`\xfa\xbet;@\x86\x07+\xa5=\xe5V\x84\x80\x9a=\x89\x91q\x16\xea\xca\xa3l\x91\xde&\xb6\x17\x1a\x96\x0e\t/\x188\x13`\xd2#\xc9T\xf4|\xd8\xcb`aO)\x94\x9a2xpTSPs28USU8' 

Si concatena los bloques “correctos”, obtendrá el texto simple esperado:

 Hello, world!ImiAq7aVLlmZDM9RfhDQgPp0CrAyZE0lyzJ6HDq4VoUmIiKUg7i2xpTSPs28USU8 

Esto es realmente extraño. Definitivamente estoy haciendo algo mal en el extremo de Python ya que las cosas se pueden descifrar, pero no de una vez. Si alguien puede ayudar, estaría muy agradecido. Gracias.

Hay un par de problemas aquí. Primero, el mensaje no es un múltiplo del tamaño de bloque, y no está usando el relleno. Y segundo, y lo más importante para este problema, es que el IV tampoco es del tamaño correcto. Debería ser de 16 bytes, pero solo tiene 12. Probablemente ambas implementaciones deberían fallar con una excepción, y en la próxima revisión importante de CryptoJS, este será el caso.

Esto es lo que sucede debido a este error: cuando el contador se incrementa por primera vez, intenta incrementar el valor indefinido, porque falta el último byte de la IV. No definido + 1 es NaN, y NaN | 0 es 0. Así es como terminas obteniendo 0 dos veces.

Cuando se utiliza el modo criptográfico CryptoJS.mode.CTR (donde CTR significa contador), el vector de Initailization junto con un contador se cifra y luego se aplica a los datos para cifrar. Esto se hace para cada bloque de datos que cifras.

Explica que las diferentes partes del mensaje se descifran correctamente cuando se aplican valores diferentes a start_val , por lo que sospecho que el contador simplemente no aumenta correctamente con cada descifrado de un bloque.

Eche un vistazo al modo de cifrado de bloque: CTR en wikipedia

Precaución: tenga en cuenta que al utilizar el modo CTR, la combinación del vector de inicialización + el contador nunca debe repetirse.

Fijo. Simplemente hice que el contador comenzara con 0 dos veces. ¿Alguien sabe si esto es una vulnerabilidad?

 import struct import base64 import Crypto.Cipher.AES import Crypto.Util.Counter import pdb def bytestring_to_int(s): r = 0 for b in s: r = r * 256 + ord(b) return r class IVCounter(object): def __init__(self, prefix="", start_val=0): self.prefix = prefix self.zeroth = True self.first = False self.current_val = start_val def __call__(self): if self.zeroth: self.zeroth = False self.first = True elif self.first: self.first = False else: self.current_val += 1 postfix = struct.pack(">I", self.current_val) n = base64.b64decode(self.prefix) + postfix return n def decrypt_msg(key, msg, iv): k = base64.b16decode(key.upper()) ctr = IVCounter(prefix=iv) #ctr = Crypto.Util.Counter.new(32, prefix=base64.b64decode(iv), initial_value=0, little_endian=False) aes = Crypto.Cipher.AES.new(k, mode=Crypto.Cipher.AES.MODE_CTR, counter=ctr) plaintext = aes.decrypt(base64.b64decode(msg)) return plaintext if __name__ == "__main__": #original: key = 'b1df40bc2e4a1d4e31c50574735e1c909aa3c8fda58eca09bf2681ce4d117e11' msg = 'LwFUZbKzuarvPR6pmXM2AiYVD2iL0/Ww2gs/9OpcMy+MWasvvzA2UEmRM8dq4loB\ndfPaYOe65JqGQMWoLOTWo1TreBd9vmPUZt72nFs=' iv = 'gpG388l8rT02vBH4' decrypted = decrypt_msg(key, msg, iv) print "Decrypted message:", repr(decrypt_msg(key, msg, iv)) print decrypted