Крайне важно, чтобы ключи в симметричных алгоритмах были случайными байтами, а не паролями или другими предсказуемыми данными. Эти случайные байты должны генерироваться с помощью криптографически стойкого генератора псевдослучайных чисел (CSPRNG). Если ключи хоть как-то предсказуемы, уровень безопасности шифра снижается, и злоумышленник, получивший доступ к шифротексту, может расшифровать его.
То, что ключ выглядит как набор случайных байтов, ещё не означает, что он действительно случайный. В данном случае ключ получен из простого пароля с помощью хеш-функции, что делает шифротекст уязвимым для взлома.
Исходный код
from Crypto.Cipher import AES
import hashlib
import random
# /usr/share/dict/words from
# https://gist.githubusercontent.com/wchargin/8927565/raw/d9783627c731268fb2935a731a618aa8e95cf465/words
with open("/usr/share/dict/words") as f:
words = [w.strip() for w in f.readlines()]
keyword = random.choice(words)
KEY = hashlib.md5(keyword.encode()).digest()
FLAG = ?
@chal.route('/passwords_as_keys/decrypt/<ciphertext>/<password_hash>/')
def decrypt(ciphertext, password_hash):
ciphertext = bytes.fromhex(ciphertext)
key = bytes.fromhex(password_hash)
cipher = AES.new(key, AES.MODE_ECB)
try:
decrypted = cipher.decrypt(ciphertext)
except ValueError as e:
return {"error": str(e)}
return {"plaintext": decrypted.hex()}
@chal.route('/passwords_as_keys/encrypt_flag/')
def encrypt_flag():
cipher = AES.new(KEY, AES.MODE_ECB)
encrypted = cipher.encrypt(FLAG.encode())
return {"ciphertext": encrypted.hex()}
Зашифрованный флаг:
c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535
Решение
У нас есть исходный текст того, как выбирался ключ и происходило шифрование. Также мы знаем, что использовался AES.
В качестве ключа используется случайный пароль из словаря:
with open("/usr/share/dict/words") as f:
words = [w.strip() for w in f.readlines()]
keyword = random.choice(words)
KEY = hashlib.md5(keyword.encode()).digest()
Скачаю его через wget. Далее мне нужно перебрать все пароли, чтобы найти нужный:
from Crypto.Cipher import AES
import hashlib
CIPHERTEXT = bytes.fromhex(
'c92b7734070205bdf6c0087a751466ec13ae15e6f1bcdd3f3a535ec0f4bbae66'
)
def try_decrypt(key: bytes) -> str | None:
plaintext = AES.new(key, AES.MODE_ECB).decrypt(CIPHERTEXT)
try:
text = plaintext.decode('ascii')
except UnicodeDecodeError:
return None
return text if text.startswith('crypto{') else None
def main():
with open('words') as f:
for word in f:
key = hashlib.md5(word.strip().encode()).digest()
if flag := try_decrypt(key):
print(flag)
break
if __name__ == '__main__':
main()
Логика работы простая. У нас есть зашифрованный текст, есть варианты ключей, а также начало открытого текста crypto{. Если после попытки расшифровки мы получим строку, начало которой совпадает с crypto{, то мы нашли наш ключ. Запущу:
cu63:Passwords as Keys/ $ python solver.py
crypto{k3y5__r__n07__p455w0rdz?}
Ключ найден.