Estoy estudiando el algoritmo AES con sus variantes (ECB, CBC, OFB, CFB, etc). También sé que por fuerza bruta, probando todas las contraseñas posibles, se llegaría a descifrar cualquier archivo.
El problema que me encuento es cómo se ataca al fichero cifrado. Suponiendo que dispongo del fichero cifado, una cadena de bytes, y no dispongo de ningún tipo de información más, y quiero probar la contraseña '000001', cómo se comprueba que esa clave es o no la correcta?
Dispongo de una función de encriptado en C#, y he cifrado un archivo de texto con la contraseña 'hola', que me devuelve una cadena de bytes la cual es el fichero cifrado. Bien, pues con la función de descifrado, con la contraseña "adios", me devuelve otra cadena de bytes, sin dar error, aunque evidentemente el texto no es legible.
Sin dar ningún tipo de error, cómo podría comprobar si la contraseña que pruebo es la correcta? Si el cifrado te permite cifrar y descifrar con una contraseña distinta, aunque no te devuelva el archivo corecto, cómo sabe la máquina que esa clave no es la correcta?
NOTA: he empleado el mismo IV tanto para el cifrado como para el descifrado.
Citar...cómo se comprueba que esa clave es o no la correcta?...
Dependiendo del modo que use, y si éste verifica que el mensaje no se haya modificado en transmisión, por lo que le será más sencillo (aunque igual sigue siendo realmente complejo e inviable).
Supongamos que tenemos el siguiente
script:
aes_gcm.py:
#!/usr/bin/env python
from Crypto.Cipher import AES
def _convert2bytes(s):
return s.encode() if not (isinstance(s, bytes)) else s
def encrypt(plaintext, password):
password = _convert2bytes(password)
plaintext = _convert2bytes(plaintext)
cipher = AES.new(password, AES.MODE_GCM)
nonce = cipher.nonce
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
return nonce+tag+ciphertext
def decrypt(ciphertext, password):
password = _convert2bytes(password)
nonce = ciphertext[:16]
tag = ciphertext[16:32]
ciphertext = ciphertext[32:]
descipher = AES.new(password, AES.MODE_GCM, nonce=nonce)
return descipher.decrypt_and_verify(ciphertext, tag)
Ahora ciframos:
>>> import aes_gcm
>>> key = ("1"*32).encode()
>>> plaintext = "Hello world!"
>>> ciphertext = aes_gcm.encrypt(plaintext.encode(), key)
>>> ciphertext
b"{\xf5$'\x08\xc9\xa1\xdb\x0e\xc9D\x1cm#\xf6\x912g@+\xbe\xe1\xce\xc4\xe6\xb9=Q\t\xe5\xbbV\xb6s\xf4\x9c8\xd34\xee\xee\xecP\xed"
>>> decrypted = aes_gcm.decrypt(ciphertext, key)
>>> decrypted
b'Hello world!'
>>> decrypted.decode() == plaintext
True
>>>
Muy bien. Este modo, con el diligente uso, te permite verificar si el mensaje ha sido corrompido o si ha habido un cambio incongruente. Como por ejemplo:
>>> key = b'0' + key[1:]
>>> key
b'01111111111111111111111111111111'
>>> decrypted = aes_gcm.decrypt(ciphertext, key)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/tmp/aes_gcm.py", line 27, in decrypt
return descipher.decrypt_and_verify(ciphertext, tag)
File "/home/dtxdf/.local/lib/python3.8/site-packages/Crypto/Cipher/_mode_gcm.py", line 567, in decrypt_and_verify
self.verify(received_mac_tag)
File "/home/dtxdf/.local/lib/python3.8/site-packages/Crypto/Cipher/_mode_gcm.py", line 508, in verify
raise ValueError("MAC check failed")
ValueError: MAC check failed
>>>
Si se nota, simplemente cambié una cifra en la contraseña y generó un error; por supuesto que esto depende de la implantación, pero por lo general, generaría una excepción sin importar la librería.
En caso de que los datos cifrados no sean parte del Cifrado autenticado (https://en.wikipedia.org/wiki/Authenticated_encryption) se complicaría un poco más. Incluso de podría decir que ya no es inviable, sino que está llegando a lo imposible, pero podría hacer lo siguiente:
aes_cbc.py:
#!/usr/bin/env python
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
def _convert2bytes(s):
return s.encode() if not (isinstance(s, bytes)) else s
def encrypt(plaintext, password):
password = _convert2bytes(password)
plaintext = _convert2bytes(plaintext)
cipher = AES.new(password, AES.MODE_CBC)
ciphertext = cipher.encrypt(pad(plaintext, AES.block_size))
return cipher.iv + ciphertext
def decrypt(ciphertext, password):
password = _convert2bytes(password)
iv = ciphertext[:16]
ciphertext = ciphertext[16:]
descipher = AES.new(password, AES.MODE_CBC, iv=iv)
return unpad(descipher.decrypt(ciphertext), AES.block_size)
>>> import aes_cbc
>>> key = ("1"*32).encode()
>>> plaintext = "Hello world!"
>>> ciphertext = aes_cbc.encrypt(plaintext, key)
>>> ciphertext
b';\xa4\xd5\xffgd\xe5\x98\x06i\xab\xbal\x19\xb2\xeai\xa8\x07\xca\xbe\x17U\\)\xc1\x1d\xfc\x03\xb9\xc7\x06'
>>> aes_cbc.decrypt(ciphertext, key)
b'Hello world!'
>>> key = b'0' + key[1:]
>>> aes_cbc.decrypt(ciphertext, key)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/tmp/aes_cbc.py", line 26, in decrypt
return unpad(descipher.decrypt(ciphertext), AES.block_size)
File "/home/dtxdf/.local/lib/python3.8/site-packages/Crypto/Util/Padding.py", line 93, in unpad
raise ValueError("PKCS#7 padding is incorrect.")
ValueError: PKCS#7 padding is incorrect.
>>>
Esta vez, el caballero que nos ayudó fue el Agoritmo de relleno (https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS%235_and_PKCS%237), que por su naturaleza detectó una anomalía. Pero no siempre éste será el que nos ayudará y como mencioné, depende de la implantación. Entones lo que sí podría aliviar un poco la tensión es detectar si los caracteres son o no, imprimibles, pero claro, esto es con caracteres que mayormente usamos los humanos y no son procesados de una forma especial (como
\n,
\r,
\t, etc.) y también puede ser que desencriptemos el mensaje, pero sea un archivo, y como un archivo puede ser cualquier cosa (incluyendo caracteres
no imprimibles) puede que lo que le digo no sea del todo correcto, pero puede probar el siguiente script para ver si le guía por un camino correcto: https://asecuritysite.com/encryption/crackaes2
Manualmente el script puede posarse de la siguiente manera (eliminando el relleno):
aes_cbc.py (
sin relleno):
#!/usr/bin/env python
from Crypto.Cipher import AES
def _convert2bytes(s):
return s.encode() if not (isinstance(s, bytes)) else s
def encrypt(plaintext, password):
password = _convert2bytes(password)
plaintext = _convert2bytes(plaintext)
cipher = AES.new(password, AES.MODE_CBC)
ciphertext = cipher.encrypt(plaintext)
return cipher.iv + ciphertext
def decrypt(ciphertext, password):
password = _convert2bytes(password)
iv = ciphertext[:16]
ciphertext = ciphertext[16:]
descipher = AES.new(password, AES.MODE_CBC, iv=iv)
return descipher.decrypt(ciphertext)
>>> import aes_cbc
>>> plaintext = "Buen día mundo.".encode()
>>> len(plaintext)
16
>>> key = ("1"*32).encode()
>>> ciphertext = aes_cbc.encrypt(plaintext, key)
>>> ciphertext
b'\xbb\xf4\x016\xd4\xea\xde\x9f\x92\xc7*\xb2]\xc2\xe8\xb4\xeegt\x9dy\xd9\x14\xa8\xb1\xee\x87\x93{&`\xfa'
>>> aes_cbc.decrypt(ciphertext, key)
b'Buen d\xc3\xada mundo.'
>>> key = b'0' + key[1:]
>>> decrypted = aes_cbc.decrypt(ciphertext, key)
>>> decrypted
b'\x05\xa0?IL!\x8dW\xf1<\x97\x85\x7f\xe7\xa98'
>>> decrypted.decode()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa0 in position 1: invalid start byte
>>>
También es realmente útil indagar sobre algunos modos, que se podrían decir como clásicos, y que están en desuso, como:
* https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#ECB
* https://medium.com/asecuritysite-when-bob-met-alice/surely-no-one-uses-ecb-mode-in-aes-332ed90f29d0
Puede que lo siguiente también sea igual de útil:
* https://es.wikipedia.org/wiki/Criptoan%C3%A1lisis_diferencial#Mec%C3%A1nica_del_ataque
~ DtxdF
Lo primero muchísimas gracias por la respuesta, muy bien afinada de verdad.
Entiendo lo que planteas, pero eso serviría para texto, detectando la anomalía de que no es un byte imprimible.
Aunque la verdad, en el caso de un archivo como por ejemplo un vídeo o una ddl, siempre van a a aparecer, con lo cual, pudieran ser muchas contraseñas. Y tratandose de texto, nos encontraríamos ante el mismo problema, serían muchisimas las combinaciones de texto imprimible como para determinar cual de ellas es la clave correcta.
Es por eso que, podemos decir, que en un archivo de una longitud digamos, de apartir de 20 megas, es imposible hallar la contraseña extacta, aún disponiendo de la eternidad y con una máquina con velocidad infinita?
@Lafleur212 (https://underc0de.org/foro/index.php?action=profile;u=94836)
La fuerza bruta es desde lejos el ataque más viable contra AES. Le dejo estas respuestas a su cuestión, y además le agrego un libro que recomendó nuestro compañero que trata sobre criptografía.
Breaking AES with ChipWhisperer - Piece of scake (Side Channel Analysis 100)
https://www.youtube.com/watch?v=FktI4qSjzaE
Vídeo y canal recomendable para aprender, aunque este vídeo que se presenta en este momento es muy bueno, hallará de sobra del mismo sujeto y del mismo tema.
Attacks on Aes (128-192-256) (https://crypto.stackexchange.com/questions/58560/attacks-on-aes-128-192-256)
Una sencilla pregunta con excelentes respuestas, enlazadas en el mismo post. Lealo, le dotará de más respuestas.
Libro gratuito y en español recomendado por nuestro compañero (https://underc0de.org/foro/dudas-generales-121/alguien-tendra-este-libro-de-criptografia-con-python/msg141132/#msg141132)
~ DtxdF