Последние два года мы работали над доказательством концепции дешифратора для программ-вымогателей семейства Phobos. По причинам, которые мы объясним здесь, он непрактичен. До сих пор у нас не получилось использовать его, чтобы помочь реальной жертве. Но мы решили опубликовать результаты и инструменты в надежде, что кто-то найдёт их полезными, интересными или продолжит исследования. Опишем уязвимость и то, как мы понизили вычислительную сложность и повысили производительность дешифратора, чтобы достичь почти практической реализации. Продолжение — к старту нашего курса «Белый хакер» Фобос — внутренний и самый крупный естественный спутник Марса. А ещё это название распространённого семейства программ-вымогателей — Phobos, впервые замеченного в начале 2019 года. Программа не очень интересная — она имеет много общего с «Дхармой» и, вероятно, написана теми же авторами. Мы начали исследование после того, как за короткое время шифровальщиком Phobos были зашифрованы несколько важных польских организаций. Стало ясно, что необычна функция расписания ключей Phobos; можно даже сказать, что она сломана. Это побудило провести дальнейшие исследования в надежде создать дешифратор. Пропустим скучные детали: функции защиты от отладки, удаление теневых копий, функция обхода основного диска и т. д. Самая интересная функция для нас — это расписание ключей:  Любопытно, что вместо одного хорошего источника энтропии автор вредоноса решил использовать несколько плохих. В списке есть: Наконец, применяется переменное, но детерминированное число раундов SHA-256. В среднем для проверки ключа требуется 256 исполнений SHA-256 и одно дешифрование AES. Это сразу зазвучало в наших головах тревожными колоколами. Предполагая, что время заражения известно с точностью до 1 секунды (например, через временные метки файлов или логи), получается, что количество операций, необходимых для перебора каждого компонента, равно: Операция Количество операций QueryPerformanceCounter 1 10**7 QueryPerformanceCounter 2 10**7 GetTickCount 10**3 SystemTimeToFileTime 10**5 GetCurrentProcessId 2**30 Очевидно, что каждый компонент можно брутфорсить независимо, ведь 10**7 для современных компьютеров не так уж много. Но можно ли восстановить весь ключ целиком? Количество ключей для наивной атаки методом перебора равно: Огромное, непостижимо большое число. Ни малейшего шанса на брутфорс. Но мы не из тех, кто легко сдаётся. Начнём с некоторых предположений. Одно мы уже сделали: благодаря системным/файловым журналам мы знаем время с точностью до 1 секунды. Одним из источников таких данных могут быть журналы событий Windows:  События по умолчанию, которое срабатывает для каждого нового процесса, нет, но при достаточном внимании к анализу часто удается восстановить PID и TID процесса-вымогателя. Даже если установить их невозможно, поскольку PID Windows последовательны, почти всегда можно ограничить их на значительную величину. Таким образом, обычно нам не придётся перебирать всё пространство в 2**30. Предположим, что мы уже знаем PID и TID (основного потока) процесса программы-вымогателя. Не волнуйтесь, это самое большое допущение во всей этой статье. Облегчит ли это наше положение? 81 бит энтропии — слишком много, чтобы думать о брутфорсе, но кое-чего мы добились. Вот другое разумное предположение: два последовательных вызова QueryPerformanceCounter вернут аналогичные результаты. В частности, второй QueryPerformanceCounter всегда будет немного больше первого. Выполнять полный перебор обоих счётчиков нет необходимости — мы можем перебрать первый, а затем предположить время между исполнениями. Если взять код как пример, вместо можно написать: Число 10**3 определено как достаточное эмпирически. В большинстве случаев этого должно быть достаточно, хотя это всего лишь 1 мс, поэтому в случае очень неудачного переключения контекста произойдёт сбой. Всё же попробуем: 2**67 вызовов sha256 — много, но число становится управляемым: по стечению обстоятельств оно почти точно соответствует текущему хешрейту BTC. Если вместо бессмысленного сжигания электричества всю сеть BTC перепрофилировать на расшифровку жертв Phobos, то эта сеть расшифровывала бы одну жертву за секунду, предполагая, что PID и TID известны. Итак, SystemTimeToFileTime может обладать точностью в 10 мс. Но GetLocalTime — нет:  Это означает, что нужно перебрать только 10**3 вариантов, а не 10**5: Может быть, получится найти алгоритм лучше? Более очевидных вещей для оптимизации нет. Обратите внимание, что key[0] равен GetTickCount() \^ QueryPerformanceCounter().Low. Наивный алгоритм перебора проверит все возможные значения обоих компонентов, но в большинстве случаев добиться можно гораздо большего. Например, 4 ^ 0 == 5 ^ 1 == 6 ^ 2 = ... == 4. Нас волнует только конечный результат, поэтому значения таймера, которые в итоге являются одним и тем же ключом, можно игнорировать. Простой способ сделать это выглядит так: К сожалению, такое требует довольно больших затрат процессора (мы хотим выжать как можно больше производительности). Оказывается, есть рекурсивный алгоритм лучше. Он позволяет не тратить время на дубликаты, но довольно тонкий и не очень элегантный: Возможно, есть алгоритм проще или быстрее, но авторы при работе над дешифратором о нём не знали. Вот энтропия после этого изменения: Это было последнее снижение сложности. Чего здесь не хватает, так это хорошей реализации. Оригинальное PoC на Python тестировало 500 ключей в секунду. Для перебора 10**11 ключей ему требуется 2314 процессорных дней. Это далеко не практично. Но Python, по сути, самый медленный с точки зрения высокопроизводительных вычислений. Можно добиться большего. Обычно в подобных ситуациях мы бы реализовали собственный дешифратор (на C++ или Rust). Но здесь даже этого недостаточно. Мы решили добиться максимальной производительности и внедрить наш решатель в CUDA. Наша первая наивная версия смогла взломать 19166 ключей в минуту. У нас не было опыта работы с CUDA, мы допустили много ошибок; статистика использования графического процессора выглядела так:  Очевидно, огромным узким местом оказался sha256: вызовов sha256 в 256 раз больше, чем AES. Большая часть работы здесь сосредоточена на упрощении и адаптации кода. Например, мы встроили sha256_update:  Встроили sha256_init:  Заменили глобальные массивы локальными переменными:  Жёстко задали размер данных в 32 байта:  И сделали несколько операций более подходящими для графического процессора, например, для bswap использовали __byte_perm. В конце концов, изменился основной цикл:  После этой оптимизации мы поняли, что код теперь делает много ненужных копий и переносов данных. Сейчас копии не нужны:  Всё это позволило нам повысить производительность в 2,5 раза — до 50 тыс. ключей в минуту.  Работа разделена на потоки, и потоки выполняют логические операции. В частности, memcopy на графическую карту и с неё может выполняться одновременно с нашим кодом без потери производительности. Просто изменив наш код для более эффективного использования потоков, мы смогли удвоить производительность до 105 тыс. ключей в минуту:  Несмотря на все эти изменения, мы даже не пытались оптимизировать AES. После всех полученных уроков это было довольно просто. Мы просто искали шаблоны, которые плохо работали на GPU, и делали код лучше. Например:  Мы изменили наивный цикл for на развёрнутую вручную версию, которая работала с 32-битными целыми. Это может показаться мелочью, но на самом деле пропускная способность стала значительно выше:  В запасе у нас был последний трюк — запускать один и тот же код на нескольких GPU! Удобно, что у нас есть небольшой кластер GPU на CERT.PL. Мы выделили две машины с общим количеством графических процессоров Nvidia в 12. Пропускная способность сразу увеличилась почти до 10 миллионов ключей в минуту. Таким образом, перебор 10**11 ключей в кластере занимает всего 10 187 секунд (2,82 часа). Звучит заманчиво, правда? К сожалению, как мы упоминали в начале, существует множество практических проблем, которые мы кратко рассмотрели, это даже усложнило публикацию дешифратора: Вот почему мы решили опубликовать эту статью и исходный код (почти работающего) дешифратора. Надеемся, что это позволит исследователям вредоносных программ по-новому взглянуть на тему и, возможно, даже расшифровывать жертв программ-вымогателей. Мы опубликовали исходный код CUDA. Он содержит краткую инструкцию, пример конфигурации и набор данных для проверки программы. Проанализированный образец программы-вымогателя: 2704e269fb5cf9a02070a0ea07d82dc9d87f2cb95e60cb71d6c6d38b01869f66 | MWDB | VT Полезная теория и интересная практика — на наших курсах:  Data Science и Machine Learning Python, веб-разработка Мобильная разработка Java и C# От основ — в глубину А также