# Crypto райтапы на CC (кроме Firmware) Итак, да начнется погружение в мир 'Заметим, что...'! *** ## Encryppcino ### Описание >Кажется, кто-то забыл пароль от своих зашифрованных файлов. Очень странно, ведь эта программа специально была создана для того, чтобы пароли от зашифрованных файлов было легко запоминать. >[Архив с материалами](https://cyberchallenge.rt.ru/files/6dd5bcadcd8d03cdb99defe24ad0e00d/encrypccino.tar.gz) ### Что дано Нам дан архив __encrypccino.tar.gz__, после его распаковки видим следующее: * скрипт __`encrypccino.py`__; * файл __`prose.txt`__ (plaintext); * его зашифрованная версия __`prose.txt.enc`__ (ciphertext); * зашифрованный (вероятно, той же фразой (об этом позже)), флаг __`flag.txt.enc`__; * файл __`words.txt`__, представляющий из себя набор слов. ### Вступление Скрипт умеет шифровать и расшифровывать файлы (опции ```-e, --encrypt``` и ```-d, --decrypt``` соответственно). При шифровании скрипт записывает зашифрованный файл с именем, равным ```имя_исходного_файла + .enc```, и выводит парольную фразу, которая генерируется с помощью выбора двух случайных слов из **words.txt**. Для расшифровки нужно ввести имя зашифрованного файла без ```.enc```, также необходимо знание парольной фразы, сгенерированной при шифровке. В данном случае брутфорс возможен, но есть путь получше, далее мы его обсудим. ### Детали алгоритма Шифрование происходит при помощи алгоритма AES-CBC, примененного 2 раза подряд, причем ключ и IV(Initialization Vector) - это 1 и 2 половины от SHA256(word + "ENCRYPCCINO") соответственно, каждый раз перед шифрованием применяется padding по схеме [PKCS#7](https://ru.wikipedia.org/wiki/%D0%94%D0%BE%D0%BF%D0%BE%D0%BB%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5_(%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F)#PKCS7) до кратности длины сообщения 16 байтам. Дешифровка - применение дешифрования AES-CBC с теми же параметрами (AES-CBC - симметричный шифр) 2 раза подряд с перевернутым порядком слов. ### Уязвимость Дело в том, что AES-CBC переводит plaintext в ciphertext той же длины. То есть при первой итерации применяется padding, и длина сообщения становится кратна 16 байтам. Затем применяется AES-CBC, и зашифрованное сообщение сохраняет свою длину, т.е. она по прежнему кратна 16 байтам. Можно заметить, что на второй итерации к сообщению снова применяется padding, но т.к. длина сообщения уже кратна 16 байтам, то он прибавит 16 байт, равных 16, в конец сообщения, и только потом зашифрует второй раз. Это значит, что можно перебирать не набор из 2 слов, а сначала второе слово, и потом только первое. ### Алгоритм решения Перебираем все слова, пытаемся дешифровать с необходимыми параметрами и проверяем, есть ли на конце 16 байт, равных 16. Это дает нам единственное возможное второе слово. Дешифруем им, убираем padding, далее снова перебираем слова и дешифруем результат ими, но на этот раз проверяем, равно ли дешифрованное исходному тексту, к которому был применен padding. Это дает нам единственное возможное первое слово. Далее используем данный в условии скрипт и получаем флаг. ### PoC (Proof of Concept) Скрипт, решающий таск (использовать в той же папке, работает 10-20 секунд): ```python= #!/usr/bin/env python3 import hashlib from os import system from Crypto.Cipher import AES # pip install pycryptodome from Crypto.Util.Padding import pad, unpad # needed library SALT = b"ENCRYPCCINO" def cipher(password): digest = hashlib.sha256(password.encode() + SALT).digest() key, iv = digest[:16], digest[16:] return AES.new(key, AES.MODE_CBC, iv) with open('prose.txt', 'rb') as f: p = f.read() with open('words.txt') as f: words = f.readlines() for i in range(len(words)): words[i] = words[i].strip() with open('prose.txt.enc', 'rb') as f: c = f.read() for i, word in enumerate(words): new_data = cipher(word).decrypt(c) if new_data[-16:] == b'\x10' * 16: second_word = word break first_dec = unpad(cipher(second_word).decrypt(c), 16) for i, word in enumerate(words): new_data = cipher(word).decrypt(first_dec) if new_data == pad(p, 16): first_word = word break # print(first_word, second_word) # uncomment if you want passphrase system(f'./encrypccino.py -d "{first_word} {second_word}" flag.txt') with open('flag.txt') as f: print(f.read()) ``` Пароль: "charissa mastocarcinomata", и мы получаем флаг: **CC{all_i_w4nt3d_w4s_a_D0UBL3_3ncrypcc1n0}** *** ## I'm one yet many ### Описание >Кажется, кто-то запустил эту программу и сохранил ее вывод в <code>output.txt</code>. Жаль, что все зашифровано... >[output.txt](https://cyberchallenge.rt.ru/files/3e7c928b7d476fc23706160ad7f15022/output.txt) >[Программа](https://cyberchallenge.rt.ru/files/dc45299d7ee387b736916325af64f12c/one-yet-many.py) ### Что дано Нам даны следующие файлы: * __`output.txt`__ - вывод программы; * __`one-yet-many.py`__ - шифрующая программа. ### Вступление Нам дан вывод этой программы, запущенной 1 раз, причем первая строка - зашифрованный флаг. ### Детали алгоритма Данная програма реализует побайтовый [one time pad aka шифр Вернама](https://ru.wikipedia.org/wiki/%D0%A8%D0%B8%D1%84%D1%80_%D0%92%D0%B5%D1%80%D0%BD%D0%B0%D0%BC%D0%B0) с переиспользованной гаммой(ключом), сгенерированной с помощью [os.urandom](https://docs.python.org/3/library/os.html#os.urandom), так что вариант взлома [PRNG](https://ru.wikipedia.org/wiki/%D0%93%D0%B5%D0%BD%D0%B5%D1%80%D0%B0%D1%82%D0%BE%D1%80_%D0%BF%D1%81%D0%B5%D0%B2%D0%B4%D0%BE%D1%81%D0%BB%D1%83%D1%87%D0%B0%D0%B9%D0%BD%D1%8B%D1%85_%D1%87%D0%B8%D1%81%D0%B5%D0%BB) отпадает. Большинство данных берутся с некоего файла __data.bin__, так что [crib dragging](https://samwho.dev/blog/toying-with-cryptography-crib-dragging/) тоже, вероятно, не сработает. Что делать? ### Уязвимость Строки считываются с помощью метода .readlines() и шифруются без всякой обработки. Заметим, что при считывании этим методом в конце каждой строки обязательно есть '\n'. То есть если мы сможем найти строки длиной 1, 2, ..., (длина флага) байт, то мы сможем полностью восстановить часть ключа, необходимую для восстановления флага, т.к. из '\n' xor Ki = Ci следует Ki = Ci xor '\n', где Ki и Ci - i-е байты ключа и шифртекста соответственно. Будем надеяться, что нам повезет найти все необходимые длины строк. ### Алгоритм решения Выделяем строку, которая является зашифрованным флагом. В остальном массиве строк находим строки длиной 1, 2, ..., 130 байт (длина флага). Нам повезло, такие строки есть для любой необходимой длины. Берем у каждой такой строки последний байт, ксорим его с '\n' и получаем соответствующий байт ключа. Далее ксорим флаг с ключом и получаем флаг. ### PoC (Proof of Concept) Скрипт, решающий таск (использовать в той же папке): ```python= #!/usr/bin/env python3 from binascii import unhexlify with open("output.txt", "rb") as f: data = f.readlines() enc_flag = data[0].strip() data = data[1:] for i in range(len(data)): data[i] = data[i].strip() diff_len_data = list() for i in range(1, len(enc_flag) // 2 + 1): for j in data: if len(j) == 2 * i: diff_len_data.append(j) break key = bytearray() for k in range(len(enc_flag) // 2): key.append(ord(unhexlify(diff_len_data[k][-2:])) ^ ord('\n')) flag = '' for i in range(len(key)): flag += chr(key[i] ^ ord(unhexlify(enc_flag[2 * i:2 * (i + 1)]))) #print(key.hex()) # uncomment if you want to see the key print(flag) ``` Ключ в hex: 39f5cca2cf5b92f93b8479a45c4f155ca73e2c2d646473c9d3281f03a7697640020d0e5bbd98ae8456a7f32a1b5b62d9ff9276beac59f2cb59c88eed812961b011514727ca9394ede33eb4dc55be5fe71cec562c14ab513a6510bf468003726d0c5caef59255c1c261a4a337c96ead1af29f268962accacec6f10e39c0a741029836 Флаг: **CC{w0w_s0_crypt0_xe7eimaix7eic0iephohng7ahvaT9Aech0oB2ieSeis2duuphaef4wiulao6AiGhu0aiQuaeNg0wi7maitouyeighohree7Nueliaitaece1ugh}** *** ## Заключение Таски были не из простых, но с некоторыми мне все же удалось справиться. Надеюсь, что вам понравился мой первый райтап. Увидимся!