История одного «сломанного» тестового задания или осторожнее с версиями OpenSSL…

Disclaimer. Я не «настоящий сварщик», но, в связи с поиском интересной работы в сфере информационной безопасности, в последнее время регулярно решаю разные CTF и машинки на HackTheBox. Поэтому, когда мне прислали ссылку на одно из тестовых заданий в стиле CTF, я не смог пройти мимо…



Смысл тестового задания достаточно простой. Дан дамп трафика, в котором спрятан ключ шифрования, некий мусор и зашифрованный флаг. Нужно их извлечь и расшифровать флаг. Также приведена команда OpenSSL, с помощью которой был зашифрован данный флаг. Трафик достаточно интересный, но уже через 10 строк кода на питоне передо мной лежал ключ шифрования, мусор и зашифрованный флаг. Казалось бы, что может пойти не так?

В задании сказано, что флаг был зашифрован приблизительно такой командой (некоторые несущественные параметры я опустил и изменил алгоритм шифрования).

echo "FLAG_xxxx…xxxxxx" | openssl enc -e -base64 -aes-256-cbc -nosalt -k $password 

Я вставил полученные из трафика параметры в команду, запустил и … получил мусор! Попробовал еще раз. Снова мусор. Попробовал пересобрать трафик разными способами. Нет, судя по всему, трафик собрать можно только однозначно. Но на выходе шифрования снова мусор!!! При этом OpenSSL честно предупреждает, что получать так ключ из пароля в 1 проход плохая идея…

echo "ENCRYPTED_FLAG" | openssl enc -d -base64 -aes-256-cbc -nosalt -k $key 
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.

Следующие сутки ушли в попытках взломать это безумие. Я разумно рассудил, что видимо я неправильно вычленил указанный в условиях «мусор» и написал несколько вариантов деления полученной строки на «пароль» и «зашифрованный флаг» для последующего «брутфорса». Не помогло… Начал глубоко разбираться в каждом из параметров.

Как мы знаем, для работы AES нужен ключ шифрования и IV (вектор инициализации). Параметр -k дает нам возможность использовать текстовую фразу, из которой уже сам OpenSSL получает нужный ключ и IV. Увидеть их можно с помощью параметра -p.

echo "FLAG_123" | openssl enc -e -base64 -aes-256-cbc -nosalt -p -k "password"
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
key=5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
iv =3B02902846FFD32E92FF168B3F5D16B0
C11kA+GcqkU4ocOvZAVr3g==

Это знание тоже ничего мне не дало. Тогда я всё же решил вернуться к самой безумной идее, которая возникала у меня. А именно: проблема не во мне, а что-то поменялось в OpenSSL…
Трафик датировался 2016 годом, поэтому я взял Ubuntu 14.04 и, без особой надежды на успех, просто вставил в неё первоначальные данные. И внезапно вместо мусора получил ФЛАГ! Вечер переставал быть томным… Более того, одна и та же команда с тем же паролем и параметром -p выдавала совершенно разные ключи шифрования и IV!

НОВАЯ СИСТЕМА (openssl 1.1.1h)

echo "FLAG_123" | openssl enc -e -base64 -aes-256-cbc -nosalt -p -k "password"
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
key=5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
iv =3B02902846FFD32E92FF168B3F5D16B0
C11kA+GcqkU4ocOvZAVr3g==

СТАРАЯ СИСТЕМА (openssl 1.0.1f)

echo "FLAG_123" | openssl enc -e -base64 -aes-256-cbc -nosalt -p -k "password"
key=5F4DCC3B5AA765D61D8327DEB882CF992B95990A9151374ABD8FF8C5A7A0FE08
iv =B7B4372CDFBCB3D16A2631B59B509E94
R3N+5v3zOz9QcNt08cwqcA==

Стало понятно, что опасения подтвердились. Изменился алгоритм генерации Key и IV из парольной фразы, что полностью сломало возможность «в лоб» решить CTF на современных версиях OpenSSL. В процессе поиска нюансов реализации я наткнулся на очень интересную работу «Password-based OpenSSL Encryption Analysis of Key Derivation Protocol» и всё стало на свои места. Вкратце, в версии 1.1.0 был добавлен новый протокол генерации ключей из пароля PBKDF2, но, что более важно в старом алгоритме PBKDF1 изменен алгоритм хеширования «по умолчанию» с MD5 на SHA-256! Таким образом, один и тот же пароль выдает разные Key и IV. Для того, чтобы расшифровать зашифрованное ранее, в новых версиях нужно использовать параметр -md md5.

“-md messagedigest: specify the message digest used for key derivation from md2, md5, sha, or sha1

После добавления этого параметра получить флаг стало возможно и на новом OpenSSL. Уж не знаю, действительно ли такой «нюанс» был учтен разработчиками тестового задания или они просто не проверяли его на современных системах, но факт остаётся фактом, пришлось глубоко погрузиться в некоторые тонкости OpenSSL.

P.S. Через знакомых я уже сообщил разработчикам тестового о найденной проблеме, а то вдруг они очень удивляются, что это люди к ним не идут…
@HappyGroundhog
26.12.2020 18:46 UTC
Первоисточник

Комментарии

@romancelover
26.12.2020 14:25 UTC
+3

Как-то раз столкнулся с этим, когда файл, зашифрованный openssl'ем на старой системе, не хотел расшифровываться на новой.
В гугле информация о несовместимости версий находится легко по словам вроде "openssl decrypt file encrypted on old version". Сложность в том, чтобы понять, что проблема из-за разницы версий OpenSSL.

@HappyGroundhog
26.12.2020 14:28 UTC
+2
Это да) Тут не были указаны версии, только команда шифрования, да и сам формат тестового в виде CTF подразумевал, что я банально мог накосячить в вычленении данных из дампа. Но кто ж знал то)
@savostin
26.12.2020 17:46 UTC
0
Вообще OpenSSL в последнее время чудит. И в C++ API критические изменения и вообще.
Я понимаю, конечно, бесплатно — чего жаловаться.
Но вроде бы считается хорошим тоном менять API только в мажорных версиях…
@HappyGroundhog
26.12.2020 18:03 UTC
+1
Меня бы вполне устроило примечание в Wiki на странице enc вроде «В версии до 1.1 мы использовали для генерации ключа md5, а с 1.1 перешли на Sha256. Будьте осторожны, это ломает совместимость!». Тогда бы я сразу обратил внимание на скромный параметр -md :) Я когда гуглил, нашел на гитхабе кучу открытых задач, связанных с этим неявным изменением… Тут, конечно, сам дурак, но проведенный тест на друзьях показал, что я не одинок в своём незнании этой детали…

@VolCh
26.12.2020 18:06 UTC
0

Не у всех проектов циферки в версиях являются major.minor.patch в смысле semver. OpenSSL точно к semver не относится.

@Serge78rus
26.12.2020 18:40 UTC
0
OpenSSL имеет API на C, а не на C++.
26.12.2020 18:41 UTC
0
Конечно, Вы правы, использую обертку.
26.12.2020 19:37 UTC
0
Я тоже давно не работал с C API OpenSSL напрямую. У меня сейчас несколько долгоиграющих проектов, использующих библиотеку POCO, в частности и как прослойку к OpenSSL. Переход с OpenSSL 1.0.X на 1.1.X прошел вообще незаметно, несмотря на то, что сейчас POCO находится в полумертвом состоянии.

Только прошу не считать это рекламой POCO, сейчас я вряд ли бы стал с ней связываться.
@romancelover
26.12.2020 18:14 UTC
+1

Ещё и API они поломали, из-за чего старый софт требует изменения исходных кодов для сборки с новым OpenSSL. Работаю с Postgres, привык к 3-й версии PgAdmin'a, а он с новой версией OpenSSL не собирается.
Хорошо, что нашёлся патч для сборки с новым OpenSSL, он довольно большой, несколько сот изменённых строк. То же самое относится и к Qt 4 (хотя я его ставил без патча, просто отключив поддержку OpenSSL, мне она не нужна была).


Это одна из самых противных системных библиотек — часто ломается совместимость старых версий с новыми. Она вызывает больше всего проблем при переносе старых версий ПО на новые системы.

@rrrad
26.12.2020 18:43 UTC
+2

А что вы хотите? В наше время понятие "безопасный алгоритм" довольно часто устаревает. А эта библиотека — кладезь алгоритмов относящихся к безопасности. Лучше чтобы по умолчанию использовались дырявые алгоритмы?

@noize
26.12.2020 20:00 UTC
-2
Где-то слышал недавно, что Openssl пишут математики, а не программисты. Вот и результат. Люди совершенно не в теме того, как надо правильно вести версионирование.
@Goose-Iron
26.12.2020 20:24 UTC
0
Когда-то очень давно программисты были математиками…
@
27.12.2020 05:49 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
@HappyGroundhog
27.12.2020 05:50 UTC
+5
К сожалению, в мануале данное поведение не описано…
27.12.2020 11:14 UTC
+1
Как-то напоролся… При переезде на более новую версию OpenSSL сломалась проверка цифровой подписи. Всё прекрасно компилируется, но не работает. Что-то поменялось в нутре функций EVP_Verify*. Помог переезд на EVP_VerifyDigest*.
Когда добавляются новые, более правильные функции — это прекрасно.
Когда старые функции обьявляются как deprecated — это нормально.
Когда старые функции (не deprecated) начинают работать как-то не так — это плохо.