TimeLock (https://www.algomachines.com/) — бесплатная программа, разрабатываемая для облегчения безопасной временной блокировки файлов размером до 10 КБ, при этом разблокировка возможна только во время временное окно, указанное пользователем во время первоначальной блокировки.

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

Создатель программного обеспечения запустил испытания TimeLock Challenges, чтобы протестировать часть алгоритма шифрования с проверкой времени на наличие уязвимостей. Это означает, что вместе с паролем предоставляется сейф, и цель состоит в том, чтобы открыть его до времени начала, для которого он был разработан.

Задача V3.3

Этот челлендж был запущен 11-м (вы можете просмотреть их все здесь, вместе с отчетами о взломанных челленджах: https://www.algomachines.com/people).

Время начала: 09.02.2020 00:00:00 UTC

Время окончания: 09.03.2020 00:00:00 UTC

Пароль: CD5FFCAB-4582–41E9-A424–9599B50F71B7

Текущая версия очень похожа на V3.2, с основными изменениями в способе отправки текста сценария в интерпретатор и в том, как интерпретатор защищает от отладки. Приложение также необходимо перезапускать при каждой попытке разблокировки, что, на мой взгляд, излишне. Это не сильно мешает попыткам взлома (отладчики могут мгновенно перезапустить приложение, поэтому взломщики замедляются примерно на 2 секунды за попытку разблокировки).

Поскольку все версии 3.x до сих пор были очень похожи, я буду ссылаться на отчеты V3.1 и V3.2, так как информация, собранная из обоих, использовалась для создания взлома для этого.

Версия 3.1: https://medium.com/@elronvhubbard/timelock-v3-1-challenge-vulnerability-report-2286a14f1f1f

Версия 3.2: https://medium.com/@elronvhubbard/timelock-v3-2-challenge-vulnerability-report-24afdaef06af

Адреса, упомянутые в этом отчете, являются виртуальными адресами (VA), перебазированными в 0x0, или смещениями файлов (FO).

Анти-отладка

И снова ничего не изменилось со времени предыдущей сборки, а это означает, что два исправления файлов, которые работали ранее, также выполнят свою работу здесь (см. отчет для версии 3.1). Точное расположение исправлений: FO 21F3F8 (4 NOP) и 43251 (2 NOP).

Анализ

Поскольку точкой уязвимости для предыдущих двух версий была «magic_decode_function», это первое место, на которое я обратил внимание в текущей версии. В версии 3.3 «magic_decode_function» имеет номер VA 83540.

Сравнивая с V3.2, мы можем отметить, что структура очень похожа. У нас все еще есть вызовы функций, которые, кажется, декодируют строки. Я потратил довольно много времени, пытаясь разобраться, что там происходит, но ничего хорошего не нашел. Не говорю, что это невозможно, но я нашел более простой способ. Ниже приведен очень короткий пример того, как теперь выглядят функции (некоторые названия могут быть неточными).

На что обратить внимание: WorkbenchLib::AddExternalFunction по-прежнему использует соглашение о строках V3.2. Другими словами, на приведенном выше изображении v12 по-прежнему содержит открытый текст, а именно — «Хэш». Однако, как только все внешние функции связаны с интерпретатором, появляется новая функция (называемая нетворчески Decrypt_string) вместо старой. Этот не пропускает открытый текст при возврате, и я также безуспешно пытался залезть внутрь него.

Ну хорошо, что мы можем сделать сейчас? Здесь важно помнить, что у нас есть исходный (не интерпретирующий) скрипт в бинарном файле версии 3.1, а также полная утечка исходного кода скрипта в версии 3.2. Конечно, первая идея, которая у меня была, заключалась в том, что я мог бы каким-то образом изменить строку, которая загружается в буфер скрипта, прямо как кряк V3.2, но я слишком тупой для этого.

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

double diff = C — Ctarget;
uint64 c_64;
if (diff < 0)
{
 c_64 = 1 — diff;
}
if (diff > 0)
{
 c_64 = diff;
}
uint64 seed = t3 + c_64;
c_64 += t3 * t3;
uint64 c_ret;
Hash(c_64, seed, c_ret);

Вышеприведенный только последний фрагмент, но самое интересное там. По сути, некоторая математика передается в 2 переменные, c_64 и seed, а затем с их помощью создается хэш. Затем хеш сравнивается с хешем, рассчитанным из заголовков блокчейна (мы знаем это из V3.1).

Но подождите, функция хеширования не запускается в интерпретаторе! Он должен присутствовать где-то в обычном коде… Если мы сможем его найти, мы можем просто управлять входными параметрами для него (c_64 и seed) и заставить его выдать хэш, который расшифрует наш сейф. Итак, как мы можем найти его?

Фрагмент IDA, который я разместил выше, имеет вызов функции WorkbenchLib::AddExternalFunction, и мы уже видели с помощью отладчика, что декодированная строка для него — «Hash». Таким образом, справедливое предположение состоит в том, что это добавляет ссылку на хеш-функцию в интерпретатор. Один из передаваемых параметров — «sub_88270», указатель на функцию. Так что, вероятно, эта функция запускается, когда строка Hash(c_64, seed, c_ret); выполняется интерпретатором. Давайте посмотрим на функцию.

По большей части просто больше вызовов функций интерпретатора, но что это? Один-единственный звонок, который выглядит неуместным…. На этом этапе стоит вернуться к версии 3.1, чтобы посмотреть, как там вызывалась функция хеширования:

Параметры идентичны, и если мы попытаемся изучить функцию, она выглядит так же. Мы в деле!

Как только я увидел это, я быстро открыл V3.2 и приступил к тестированию. Почему? Потому что у меня уже был способ контролировать ввод в функцию, и я хотел посмотреть, чем плохой ввод отличается от хорошего ввода.

Пересмотренная версия 3.2

Итак, давайте снова посмотрим на V3.2. Неудивительно, что WorkbenchLib::AddExternalFunction для Hash находится в том же месте внутри magic_decode_function. Давайте запустим отладчик и установим точку останова на VA 794B9, где находится вызов Hash. Откройте вызов V3.2 (с паролем 28FDC1A8–7320–4885-B03E-85FC06DCD4A3) и подождите, пока не сработает bp. Когда мы туда доберемся, начальное значение будет в регистре R8, и его значение должно быть seed=AEFD. Регистр RCX содержит указатель на местоположение c_64, поэтому, если мы просматриваем область памяти в этом месте, мы находим c_64=779B8811. Эти значения относятся к закрытому ящику.

Теперь давайте посмотрим на те же переменные, но на этот раз для разблокируемого сейфа. Не забудьте также сохранить точку останова на VA 794B9. Следуя отчету V3.2, мы можем исправить строку скрипта double diff = C — Ctarget; intodouble diff = 0,25; а затем мы продолжаем выполнение, и наша новая точка останова срабатывает. На этот раз seed=AEFC и c_64=779B8810. Хорошо, давайте сделаем небольшой мафф.

uint_64 c_64 = diff or 1 - diff
uint_64 seed = t3 + c_64
c_64 += t3*t3
seed_bad  = AEFD
seed_good = AEFC
c_64_bad  = 779B8811
c_64_good = 779B8810

Теперь c_64 и seed — целые числа. А поскольку diff должен быть меньше 0,5, мы можем извлечь правило для «хороших» значений (поскольку округление меньше 0,5 даст 0):

seed_good = t3    + (diff_should_be_0) = t3
c_64_good = t3*t3 + (diff_should_be_0) = t3*t3
-> t3    = AEFC
-> t3*t3 = 779B8810

Теперь давайте проверим нашу гипотезу. Мы перезапускаем процесс разблокировки для V3.2 и не останавливаемся на изменении каких-либо строк скрипта, а используем только один бп при вызове хеш-функции. Как только это произойдет, мы изменим R8 с AEFD на AEFC и значение указателя RCX с 779B8811 на 779B8810. Теперь мы можем продолжить выполнение и сделать это еще 2 раза (как обычно, расшифровка выполняется 3 раза). Бум, мы снова прошли Challenge V3.2!

Пришло время применить наши новые знания в Challenge V3.3.

Открытие сейфа

Для TimeLock V3.3 вызов хеш-функции исходит из VA 88359, поэтому давайте поставим точку останова там. Мы можем начать процесс разблокировки, и как только выполнение кода остановится на нашем bp, мы можем прочитать R8 и значение указателя RCX, чтобы получить:

seed_bad = 9EFC3
c_64_bad = 62BC1BEF83

Мы знаем, что c_64 = t3*t3 и seed = t3, поэтому c_64 = seed * seed. Если мы сами посчитаем:

9EFC3 * 9EFC3 = 62BC43AE89

Мы превысили значение, которое показывает нам код. Значение «семя» неверно. Но мы и так это знали. Попробуем его уменьшить.

9EFC2 * 9EFC2 = 62BC2FCF04
9EFC1 * 9EFC1 = 62BC1BEF81 <---- CLOSE!

Итак, похоже, что в данном случае seed = t3 +2, что означает, что diff округляется до 2.Поскольку c_64 = t3 * t3 + diffмы знаем, что нам также нужно вычесть 2 из c_64_bad, что даст нам новые значения, которые должны открывать сейф:

t3        = 9EFC1
seed_good = 9EFC1
c_64_good = 62BC1BEF81

Поскольку мы все еще находимся в этой точке останова, давайте изменим R8 на 9EFC1 и значение указателя RCX на 62BC1BEF81. Продолжите выполнение и сделайте это еще два раза. НЕТ, не получилось! Ох, наша математика неверна?

Что ж, на данный момент стоит внимательно прочитать примечания к патчу последней версии, поскольку они упоминают:

Улучшенные меры защиты от отладки интерпретатора.

Ну, я не видел… ох, подождите. Может быть, интерпретатор видит, что мы его отлаживаем, и балуется с нами. Но, как мы знаем из версии 3.1, нас волнует только вывод хэша, больше нас не интересует выполнение. И мы можем проверить, какой хэш возвращается в основной код (как в версии 3.1) из функции magic_decrypt_function.

В V3.3 возвращаемое значение можно найти в VA 5DB75в регистре RAX (небольшое примечание: RAX всегда содержит возвращаемое значение функции. Параметры функции могут также изменяться, если они передаются по ссылке, например, при использовании ключевых слов out и ref в вызове функции в коде C++). Так что установите bp в этом месте, но сохраните и старый. Как только будет достигнута первая точка останова (вызов Hash), снова измените значения, используя то, что мы получили выше, и продолжите выполнение. Мы достигаем новой точки останова, а RAX равен 0! Там определенно происходит что-то подозрительное.

Но, как я уже сказал, к счастью для нас, нас интересует только вывод хеш-функции. При желании весь сценарий интерпретатора можно пропустить, если у нас есть правильный хэш. И как показывает вызов хэш-функции V3.1:

вывод сохраняется в 4-м параметре, что означает R9. Точнее, R9 будет содержать указатель на хэш.

Итак, продолжайте выполнение, и вы снова окажетесь на вызове Hash. Мы снова меняем значения на «хорошие», но на этот раз вместо возобновления выполнения мы просто перешагиваем этот вызов. В этот момент хэш-функция закончила обработку чисел, и R9 удерживает указатель на вычисленный хэш. Для справки, этот хэш должен быть E6DB12699A51BE6D.

Теперь мы можем возобновить выполнение, и мы снова окажемся в регистре RAX, который равен 0. Однако на этот раз мы готовы; мы присваиваем RAX хэш сверху. Продолжайте выполнение снова, и мы находимся на вызове Hash. Мы даже не заботимся об этом больше, это послужило своей цели. Продолжить выполнение. Наконец, мы в последний раз видим пустой RAX. Замените 0 на вычисленный хэш. Продолжить выполнение снова. Бум! Мы получили нашу расшифровку!

Подтверждение

Эта версия оказалась гораздо более сложной, чем предыдущая. Обфускация кода интерпретатора кажется (по крайней мере мне) довольно сильной, и я пока не нашел способа модифицировать ее с помощью отладчика. Очевидно, теперь существует проблема, связанная с тем, что самой хеш-функцией можно злоупотреблять. Можно предположить, что вся хэш-функция может быть интерпретирована и во время выполнения, хотя она довольно длинная. Некоторые встроенные константы хэш-функций должны быть изменены, поскольку в противном случае уязвимые версии TimeLock (такие как эта) могут быть использованы для разблокировки будущих задач.

Я по-прежнему считаю, что дальнейшая обфускация после сборки (с использованием некоторых бесплатных обфускаторов с открытым исходным кодом) обеспечит дополнительную безопасность.

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